@naeemo/capnp 0.8.1 → 0.9.1

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.
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ import { t as SchemaNodeType } from "./schema-types-GVRD1pwE.js";
3
+ import { readFileSync } from "node:fs";
4
+
5
+ //#region src/compat/index.ts
6
+ /**
7
+ * Check compatibility between two schemas
8
+ */
9
+ function checkCompatibility(oldSchema, newSchema, options) {
10
+ const opts = {
11
+ strictRenames: false,
12
+ allowRemoveDeprecated: false,
13
+ checkUnions: true,
14
+ ...options
15
+ };
16
+ const issues = [];
17
+ const oldNodes = Array.isArray(oldSchema) ? oldSchema : [oldSchema];
18
+ const newNodes = Array.isArray(newSchema) ? newSchema : [newSchema];
19
+ const oldMap = new Map(oldNodes.map((n) => [n.id, n]));
20
+ const newMap = new Map(newNodes.map((n) => [n.id, n]));
21
+ for (const oldNode of oldNodes) {
22
+ const newNode = newMap.get(oldNode.id);
23
+ if (!newNode) {
24
+ if (oldNode.type === SchemaNodeType.STRUCT) issues.push({
25
+ type: "breaking",
26
+ category: "struct_removed",
27
+ message: `Struct '${oldNode.displayName}' was removed`,
28
+ path: oldNode.displayName,
29
+ suggestion: "Consider deprecating before removing"
30
+ });
31
+ continue;
32
+ }
33
+ compareNodes(oldNode, newNode, issues, opts);
34
+ }
35
+ for (const newNode of newNodes) {
36
+ const oldNode = oldMap.get(newNode.id);
37
+ if (oldNode) checkNewRequiredFields(oldNode, newNode, issues);
38
+ }
39
+ const breaking = issues.filter((i) => i.type === "breaking").length;
40
+ const warning = issues.filter((i) => i.type === "warning").length;
41
+ const info = issues.filter((i) => i.type === "info").length;
42
+ return {
43
+ compatible: breaking === 0,
44
+ issues,
45
+ summary: {
46
+ breaking,
47
+ warning,
48
+ info
49
+ },
50
+ oldVersion: oldNodes[0] ? {
51
+ id: oldNodes[0].id,
52
+ displayName: oldNodes[0].displayName
53
+ } : void 0,
54
+ newVersion: newNodes[0] ? {
55
+ id: newNodes[0].id,
56
+ displayName: newNodes[0].displayName
57
+ } : void 0
58
+ };
59
+ }
60
+ function compareNodes(oldNode, newNode, issues, options) {
61
+ if (oldNode.type !== newNode.type) {
62
+ issues.push({
63
+ type: "breaking",
64
+ category: "field_type_changed",
65
+ message: `Node '${oldNode.displayName}' type changed from ${SchemaNodeType[oldNode.type]} to ${SchemaNodeType[newNode.type]}`,
66
+ path: oldNode.displayName
67
+ });
68
+ return;
69
+ }
70
+ if (oldNode.type === SchemaNodeType.STRUCT && newNode.type === SchemaNodeType.STRUCT) compareStructs(oldNode, newNode, issues, options);
71
+ if (oldNode.type === SchemaNodeType.ENUM && newNode.type === SchemaNodeType.ENUM) compareEnums(oldNode, newNode, issues);
72
+ }
73
+ function compareStructs(oldNode, newNode, issues, options) {
74
+ if (!oldNode.structInfo || !newNode.structInfo) return;
75
+ const oldFields = new Map(oldNode.structInfo.fields.map((f) => [f.name, f]));
76
+ const newFields = new Map(newNode.structInfo.fields.map((f) => [f.name, f]));
77
+ for (const [name, oldField] of oldFields) {
78
+ if (!newFields.has(name)) {
79
+ issues.push({
80
+ type: "breaking",
81
+ category: "field_removed",
82
+ message: `Field '${oldNode.displayName}.${name}' was removed`,
83
+ path: `${oldNode.displayName}.${name}`,
84
+ suggestion: "Consider deprecating before removing"
85
+ });
86
+ continue;
87
+ }
88
+ compareFields(oldField, newFields.get(name), oldNode.displayName, issues);
89
+ }
90
+ if (options.checkUnions) {
91
+ if (oldNode.structInfo.discriminantCount !== newNode.structInfo.discriminantCount) issues.push({
92
+ type: "breaking",
93
+ category: "union_changed",
94
+ message: `Union in '${oldNode.displayName}' changed`,
95
+ path: oldNode.displayName,
96
+ suggestion: "Avoid changing union structure"
97
+ });
98
+ }
99
+ }
100
+ function compareFields(oldField, newField, structName, issues) {
101
+ const oldType = oldField.type.kind.type;
102
+ const newType = newField.type.kind.type;
103
+ if (oldType !== newType) {
104
+ issues.push({
105
+ type: "breaking",
106
+ category: "field_type_changed",
107
+ message: `Field '${structName}.${oldField.name}' type changed from ${oldType} to ${newType}`,
108
+ path: `${structName}.${oldField.name}`,
109
+ suggestion: "Create new field with different name instead of changing type"
110
+ });
111
+ return;
112
+ }
113
+ }
114
+ function compareEnums(oldNode, newNode, issues) {
115
+ if (!oldNode.enumInfo || !newNode.enumInfo) return;
116
+ const newValues = new Set(newNode.enumInfo.enumerants.map((e) => e.name));
117
+ for (const oldValue of oldNode.enumInfo.enumerants) if (!newValues.has(oldValue.name)) issues.push({
118
+ type: "breaking",
119
+ category: "enum_value_removed",
120
+ message: `Enum value '${oldNode.displayName}.${oldValue.name}' was removed`,
121
+ path: `${oldNode.displayName}.${oldValue.name}`,
122
+ suggestion: "Reserve enum values instead of removing them"
123
+ });
124
+ }
125
+ function checkNewRequiredFields(oldNode, newNode, issues) {
126
+ if (!oldNode.structInfo || !newNode.structInfo) return;
127
+ const oldFields = new Set(oldNode.structInfo.fields.map((f) => f.name));
128
+ for (const newField of newNode.structInfo.fields) if (!oldFields.has(newField.name)) issues.push({
129
+ type: "info",
130
+ category: "field_added_optional",
131
+ message: `New field '${newNode.displayName}.${newField.name}' added`,
132
+ path: `${newNode.displayName}.${newField.name}`
133
+ });
134
+ }
135
+ /**
136
+ * Format compatibility report as human-readable text
137
+ */
138
+ function formatReport(report) {
139
+ const lines = [];
140
+ lines.push("=".repeat(60));
141
+ lines.push("SCHEMA COMPATIBILITY REPORT");
142
+ lines.push("=".repeat(60));
143
+ lines.push("");
144
+ if (report.compatible) lines.push("✅ Schemas are compatible");
145
+ else lines.push("❌ Schemas are NOT compatible");
146
+ lines.push("");
147
+ lines.push("Summary:");
148
+ lines.push(` Breaking: ${report.summary.breaking}`);
149
+ lines.push(` Warnings: ${report.summary.warning}`);
150
+ lines.push(` Info: ${report.summary.info}`);
151
+ lines.push("");
152
+ if (report.issues.length > 0) {
153
+ lines.push("Issues:");
154
+ lines.push("-".repeat(60));
155
+ for (const issue of report.issues) {
156
+ const icon = issue.type === "breaking" ? "❌" : issue.type === "warning" ? "⚠️" : "ℹ️";
157
+ lines.push(`${icon} [${issue.type.toUpperCase()}] ${issue.category}`);
158
+ lines.push(` Path: ${issue.path}`);
159
+ lines.push(` ${issue.message}`);
160
+ if (issue.suggestion) lines.push(` 💡 ${issue.suggestion}`);
161
+ lines.push("");
162
+ }
163
+ }
164
+ return lines.join("\n");
165
+ }
166
+
167
+ //#endregion
168
+ //#region src/compat/cli.ts
169
+ /**
170
+ * Schema Compatibility Checker CLI
171
+ *
172
+ * Check compatibility between Cap'n Proto schema versions
173
+ *
174
+ * Usage: capnp compat <old-schema> <new-schema> [options]
175
+ */
176
+ const VERSION = "0.9.0";
177
+ function printUsage() {
178
+ console.log(`
179
+ Cap'n Proto Schema Compatibility Checker v${VERSION}
180
+
181
+ Usage: capnp compat <old-schema> <new-schema> [options]
182
+
183
+ Arguments:
184
+ old-schema Path to old schema JSON file
185
+ new-schema Path to new schema JSON file
186
+
187
+ Options:
188
+ --strict-renames Treat field renames as breaking
189
+ --allow-remove-dep Allow removing deprecated fields
190
+ --json Output as JSON
191
+ --quiet Only output errors
192
+ -h, --help Show this help
193
+
194
+ Examples:
195
+ capnp compat schema-v1.json schema-v2.json
196
+ capnp compat old.json new.json --json
197
+ capnp compat old.json new.json --strict-renames
198
+ `);
199
+ }
200
+ function parseArgs(args) {
201
+ const options = {};
202
+ for (let i = 0; i < args.length; i++) {
203
+ const arg = args[i];
204
+ if (arg === "-h" || arg === "--help") {
205
+ printUsage();
206
+ process.exit(0);
207
+ }
208
+ if (arg === "--strict-renames") options.strictRenames = true;
209
+ else if (arg === "--allow-remove-dep") options.allowRemoveDeprecated = true;
210
+ else if (arg === "--json") options.json = true;
211
+ else if (arg === "--quiet") options.quiet = true;
212
+ else if (!arg.startsWith("-")) {
213
+ if (!options.oldSchema) options.oldSchema = arg;
214
+ else if (!options.newSchema) options.newSchema = arg;
215
+ }
216
+ }
217
+ return options;
218
+ }
219
+ function loadSchema(path) {
220
+ const content = readFileSync(path, "utf-8");
221
+ const parsed = JSON.parse(content);
222
+ return (Array.isArray(parsed) ? parsed : [parsed]).map((node) => ({
223
+ ...node,
224
+ id: typeof node.id === "string" ? BigInt(node.id) : node.id,
225
+ scopeId: typeof node.scopeId === "string" ? BigInt(node.scopeId) : node.scopeId ?? 0n
226
+ }));
227
+ }
228
+ async function run(args) {
229
+ const options = parseArgs(args);
230
+ if (!options.oldSchema || !options.newSchema) {
231
+ console.error("Error: Both old and new schema files are required");
232
+ printUsage();
233
+ process.exit(1);
234
+ }
235
+ try {
236
+ const report = checkCompatibility(loadSchema(options.oldSchema), loadSchema(options.newSchema), {
237
+ strictRenames: options.strictRenames,
238
+ allowRemoveDeprecated: options.allowRemoveDeprecated
239
+ });
240
+ if (options.json) console.log(JSON.stringify(report, (_, v) => typeof v === "bigint" ? v.toString() : v, 2));
241
+ else if (!options.quiet) console.log(formatReport(report));
242
+ process.exit(report.compatible ? 0 : 1);
243
+ } catch (err) {
244
+ console.error("Error:", err instanceof Error ? err.message : err);
245
+ process.exit(2);
246
+ }
247
+ }
248
+
249
+ //#endregion
250
+ export { run };
251
+ //# sourceMappingURL=cli-DlZBZozW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-DlZBZozW.js","names":[],"sources":["../src/compat/index.ts","../src/compat/cli.ts"],"sourcesContent":["/**\n * Schema Compatibility Checker\n *\n * Detect breaking changes between Cap'n Proto schema versions.\n */\n\nimport type { SchemaField, SchemaNode } from '../rpc/schema-types.js';\nimport { SchemaNodeType } from '../rpc/schema-types.js';\n\nexport interface CompatibilityOptions {\n /** Treat field renames as breaking (default: false) */\n strictRenames?: boolean;\n /** Allow removing deprecated fields (default: false) */\n allowRemoveDeprecated?: boolean;\n /** Check union changes (default: true) */\n checkUnions?: boolean;\n}\n\nexport interface CompatibilityIssue {\n /** Type of compatibility issue */\n type: 'breaking' | 'warning' | 'info';\n /** Category of the issue */\n category:\n | 'field_removed'\n | 'field_type_changed'\n | 'field_default_changed'\n | 'struct_removed'\n | 'enum_value_removed'\n | 'union_changed'\n | 'capability_added'\n | 'field_added_optional'\n | 'field_added_required';\n /** Human-readable description */\n message: string;\n /** Path to the affected element */\n path: string;\n /** Suggested fix */\n suggestion?: string;\n}\n\nexport interface CompatibilityReport {\n /** Whether schemas are compatible */\n compatible: boolean;\n /** List of all issues */\n issues: CompatibilityIssue[];\n /** Summary counts */\n summary: {\n breaking: number;\n warning: number;\n info: number;\n };\n /** Old schema version info */\n oldVersion?: {\n id: bigint;\n displayName: string;\n };\n /** New schema version info */\n newVersion?: {\n id: bigint;\n displayName: string;\n };\n}\n\n/**\n * Check compatibility between two schemas\n */\nexport function checkCompatibility(\n oldSchema: SchemaNode | SchemaNode[],\n newSchema: SchemaNode | SchemaNode[],\n options?: CompatibilityOptions\n): CompatibilityReport {\n const opts = {\n strictRenames: false,\n allowRemoveDeprecated: false,\n checkUnions: true,\n ...options,\n };\n\n const issues: CompatibilityIssue[] = [];\n\n // Normalize to arrays\n const oldNodes = Array.isArray(oldSchema) ? oldSchema : [oldSchema];\n const newNodes = Array.isArray(newSchema) ? newSchema : [newSchema];\n\n // Build maps for quick lookup\n const oldMap = new Map(oldNodes.map((n) => [n.id, n]));\n const newMap = new Map(newNodes.map((n) => [n.id, n]));\n\n // Check each old node\n for (const oldNode of oldNodes) {\n const newNode = newMap.get(oldNode.id);\n\n if (!newNode) {\n // Node was removed\n if (oldNode.type === SchemaNodeType.STRUCT) {\n issues.push({\n type: 'breaking',\n category: 'struct_removed',\n message: `Struct '${oldNode.displayName}' was removed`,\n path: oldNode.displayName,\n suggestion: 'Consider deprecating before removing',\n });\n }\n continue;\n }\n\n // Compare same-id nodes\n compareNodes(oldNode, newNode, issues, opts);\n }\n\n // Check for new required fields (also breaking)\n for (const newNode of newNodes) {\n const oldNode = oldMap.get(newNode.id);\n if (oldNode) {\n checkNewRequiredFields(oldNode, newNode, issues);\n }\n }\n\n const breaking = issues.filter((i) => i.type === 'breaking').length;\n const warning = issues.filter((i) => i.type === 'warning').length;\n const info = issues.filter((i) => i.type === 'info').length;\n\n return {\n compatible: breaking === 0,\n issues,\n summary: { breaking, warning, info },\n oldVersion: oldNodes[0]\n ? { id: oldNodes[0].id, displayName: oldNodes[0].displayName }\n : undefined,\n newVersion: newNodes[0]\n ? { id: newNodes[0].id, displayName: newNodes[0].displayName }\n : undefined,\n };\n}\n\nfunction compareNodes(\n oldNode: SchemaNode,\n newNode: SchemaNode,\n issues: CompatibilityIssue[],\n options: Required<CompatibilityOptions>\n): void {\n // Check type hasn't changed\n if (oldNode.type !== newNode.type) {\n issues.push({\n type: 'breaking',\n category: 'field_type_changed',\n message: `Node '${oldNode.displayName}' type changed from ${SchemaNodeType[oldNode.type]} to ${SchemaNodeType[newNode.type]}`,\n path: oldNode.displayName,\n });\n return;\n }\n\n // Compare structs\n if (oldNode.type === SchemaNodeType.STRUCT && newNode.type === SchemaNodeType.STRUCT) {\n compareStructs(oldNode, newNode, issues, options);\n }\n\n // Compare enums\n if (oldNode.type === SchemaNodeType.ENUM && newNode.type === SchemaNodeType.ENUM) {\n compareEnums(oldNode, newNode, issues);\n }\n}\n\nfunction compareStructs(\n oldNode: SchemaNode,\n newNode: SchemaNode,\n issues: CompatibilityIssue[],\n options: Required<CompatibilityOptions>\n): void {\n if (!oldNode.structInfo || !newNode.structInfo) return;\n\n const oldFields = new Map(oldNode.structInfo.fields.map((f) => [f.name, f]));\n const newFields = new Map(newNode.structInfo.fields.map((f) => [f.name, f]));\n\n // Check for removed fields\n for (const [name, oldField] of oldFields) {\n if (!newFields.has(name)) {\n issues.push({\n type: 'breaking',\n category: 'field_removed',\n message: `Field '${oldNode.displayName}.${name}' was removed`,\n path: `${oldNode.displayName}.${name}`,\n suggestion: 'Consider deprecating before removing',\n });\n continue;\n }\n\n // Compare field types\n const newField = newFields.get(name)!;\n compareFields(oldField, newField, oldNode.displayName, issues);\n }\n\n // Check union changes\n if (options.checkUnions) {\n const oldUnionCount = oldNode.structInfo.discriminantCount;\n const newUnionCount = newNode.structInfo.discriminantCount;\n\n if (oldUnionCount !== newUnionCount) {\n issues.push({\n type: 'breaking',\n category: 'union_changed',\n message: `Union in '${oldNode.displayName}' changed`,\n path: oldNode.displayName,\n suggestion: 'Avoid changing union structure',\n });\n }\n }\n}\n\nfunction compareFields(\n oldField: SchemaField,\n newField: SchemaField,\n structName: string,\n issues: CompatibilityIssue[]\n): void {\n // Check type changes\n const oldType = oldField.type.kind.type;\n const newType = newField.type.kind.type;\n\n if (oldType !== newType) {\n issues.push({\n type: 'breaking',\n category: 'field_type_changed',\n message: `Field '${structName}.${oldField.name}' type changed from ${oldType} to ${newType}`,\n path: `${structName}.${oldField.name}`,\n suggestion: 'Create new field with different name instead of changing type',\n });\n return;\n }\n\n // Check for required -> optional (safe) vs optional -> required (breaking)\n // This is handled in checkNewRequiredFields\n}\n\nfunction compareEnums(\n oldNode: SchemaNode,\n newNode: SchemaNode,\n issues: CompatibilityIssue[]\n): void {\n if (!oldNode.enumInfo || !newNode.enumInfo) return;\n\n const newValues = new Set(newNode.enumInfo.enumerants.map((e) => e.name));\n\n for (const oldValue of oldNode.enumInfo.enumerants) {\n if (!newValues.has(oldValue.name)) {\n issues.push({\n type: 'breaking',\n category: 'enum_value_removed',\n message: `Enum value '${oldNode.displayName}.${oldValue.name}' was removed`,\n path: `${oldNode.displayName}.${oldValue.name}`,\n suggestion: 'Reserve enum values instead of removing them',\n });\n }\n }\n}\n\nfunction checkNewRequiredFields(\n oldNode: SchemaNode,\n newNode: SchemaNode,\n issues: CompatibilityIssue[]\n): void {\n if (!oldNode.structInfo || !newNode.structInfo) return;\n\n const oldFields = new Set(oldNode.structInfo.fields.map((f) => f.name));\n\n for (const newField of newNode.structInfo.fields) {\n if (!oldFields.has(newField.name)) {\n // This is a new field\n // In Cap'n Proto, all fields are effectively optional (have defaults)\n // So adding fields is safe\n issues.push({\n type: 'info',\n category: 'field_added_optional',\n message: `New field '${newNode.displayName}.${newField.name}' added`,\n path: `${newNode.displayName}.${newField.name}`,\n });\n }\n }\n}\n\n/**\n * Format compatibility report as human-readable text\n */\nexport function formatReport(report: CompatibilityReport): string {\n const lines: string[] = [];\n\n lines.push('='.repeat(60));\n lines.push('SCHEMA COMPATIBILITY REPORT');\n lines.push('='.repeat(60));\n lines.push('');\n\n if (report.compatible) {\n lines.push('✅ Schemas are compatible');\n } else {\n lines.push('❌ Schemas are NOT compatible');\n }\n lines.push('');\n\n lines.push('Summary:');\n lines.push(` Breaking: ${report.summary.breaking}`);\n lines.push(` Warnings: ${report.summary.warning}`);\n lines.push(` Info: ${report.summary.info}`);\n lines.push('');\n\n if (report.issues.length > 0) {\n lines.push('Issues:');\n lines.push('-'.repeat(60));\n\n for (const issue of report.issues) {\n const icon = issue.type === 'breaking' ? '❌' : issue.type === 'warning' ? '⚠️' : 'ℹ️';\n lines.push(`${icon} [${issue.type.toUpperCase()}] ${issue.category}`);\n lines.push(` Path: ${issue.path}`);\n lines.push(` ${issue.message}`);\n if (issue.suggestion) {\n lines.push(` 💡 ${issue.suggestion}`);\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n","/**\n * Schema Compatibility Checker CLI\n *\n * Check compatibility between Cap'n Proto schema versions\n *\n * Usage: capnp compat <old-schema> <new-schema> [options]\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { SchemaNode } from '../rpc/schema-types.js';\nimport { checkCompatibility, formatReport } from './index.js';\n\nconst VERSION = '0.9.0';\n\nfunction printUsage() {\n console.log(`\nCap'n Proto Schema Compatibility Checker v${VERSION}\n\nUsage: capnp compat <old-schema> <new-schema> [options]\n\nArguments:\n old-schema Path to old schema JSON file\n new-schema Path to new schema JSON file\n\nOptions:\n --strict-renames Treat field renames as breaking\n --allow-remove-dep Allow removing deprecated fields\n --json Output as JSON\n --quiet Only output errors\n -h, --help Show this help\n\nExamples:\n capnp compat schema-v1.json schema-v2.json\n capnp compat old.json new.json --json\n capnp compat old.json new.json --strict-renames\n`);\n}\n\nfunction parseArgs(args: string[]) {\n const options: {\n oldSchema?: string;\n newSchema?: string;\n strictRenames?: boolean;\n allowRemoveDeprecated?: boolean;\n json?: boolean;\n quiet?: boolean;\n } = {};\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === '-h' || arg === '--help') {\n printUsage();\n process.exit(0);\n }\n\n if (arg === '--strict-renames') {\n options.strictRenames = true;\n } else if (arg === '--allow-remove-dep') {\n options.allowRemoveDeprecated = true;\n } else if (arg === '--json') {\n options.json = true;\n } else if (arg === '--quiet') {\n options.quiet = true;\n } else if (!arg.startsWith('-')) {\n if (!options.oldSchema) {\n options.oldSchema = arg;\n } else if (!options.newSchema) {\n options.newSchema = arg;\n }\n }\n }\n\n return options;\n}\n\nfunction loadSchema(path: string): SchemaNode | SchemaNode[] {\n const content = readFileSync(path, 'utf-8');\n const parsed = JSON.parse(content);\n\n // Handle array or single object\n const nodes = Array.isArray(parsed) ? parsed : [parsed];\n\n // Convert string IDs to bigints\n return nodes.map((node: Record<string, unknown>) => ({\n ...node,\n id: typeof node.id === 'string' ? BigInt(node.id) : node.id,\n scopeId: typeof node.scopeId === 'string' ? BigInt(node.scopeId) : (node.scopeId ?? 0n),\n })) as SchemaNode[];\n}\n\nexport async function run(args: string[]): Promise<void> {\n const options = parseArgs(args);\n\n if (!options.oldSchema || !options.newSchema) {\n console.error('Error: Both old and new schema files are required');\n printUsage();\n process.exit(1);\n }\n\n try {\n const oldSchema = loadSchema(options.oldSchema);\n const newSchema = loadSchema(options.newSchema);\n\n const report = checkCompatibility(oldSchema, newSchema, {\n strictRenames: options.strictRenames,\n allowRemoveDeprecated: options.allowRemoveDeprecated,\n });\n\n if (options.json) {\n console.log(JSON.stringify(report, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2));\n } else if (!options.quiet) {\n console.log(formatReport(report));\n }\n\n // Exit with error code if not compatible\n process.exit(report.compatible ? 0 : 1);\n } catch (err) {\n console.error('Error:', err instanceof Error ? err.message : err);\n process.exit(2);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ import { a as decodePointer, i as PointerTag, n as WORD_SIZE, r as ElementSize, t as Segment } from "./segment-yid_PYS5.js";
3
+ import { readFileSync } from "node:fs";
4
+
5
+ //#region src/cli-audit.ts
6
+ /** 默认安全选项 */
7
+ const DEFAULT_AUDIT_OPTIONS = {
8
+ maxSegments: 64,
9
+ maxTotalSize: 64 * 1024 * 1024,
10
+ strictMode: false
11
+ };
12
+ /**
13
+ * Cap'n Proto 消息审计读取器
14
+ * 用于安全审计消息文件
15
+ */
16
+ var AuditReader = class {
17
+ segments;
18
+ options;
19
+ issues = [];
20
+ visitedPointers = /* @__PURE__ */ new Set();
21
+ pointersScanned = 0;
22
+ maxNestingDepth = 0;
23
+ constructor(buffer, options = {}) {
24
+ this.options = {
25
+ ...DEFAULT_AUDIT_OPTIONS,
26
+ ...options
27
+ };
28
+ const uint8Array = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
29
+ this.segments = [];
30
+ if (uint8Array.byteLength < 8) {
31
+ this.addIssue("error", "invalid_header", "消息太小,无法包含有效的 Cap'n Proto 头部", "header", "消息至少需要8字节的头部");
32
+ return;
33
+ }
34
+ if (uint8Array.byteLength > this.options.maxTotalSize) {
35
+ this.addIssue("error", "size_exceeded", `消息大小(${uint8Array.byteLength}字节)超过最大限制(${this.options.maxTotalSize}字节)`, "header", "减小消息大小或增加 --max-size 限制");
36
+ if (this.options.strictMode) return;
37
+ }
38
+ const view = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
39
+ const firstWordLow = view.getUint32(0, true);
40
+ const firstWordHigh = view.getUint32(4, true);
41
+ const segmentCount = (firstWordLow & 4294967295) + 1;
42
+ const firstSegmentSize = firstWordHigh;
43
+ if (segmentCount > this.options.maxSegments) {
44
+ this.addIssue("error", "segment_count_exceeded", `段数(${segmentCount})超过最大限制(${this.options.maxSegments})`, "header", "减少段数或增加 --max-segments 限制");
45
+ if (this.options.strictMode) return;
46
+ }
47
+ let offset = 8;
48
+ const segmentSizes = [firstSegmentSize];
49
+ for (let i = 1; i < segmentCount; i++) {
50
+ if (offset + 4 > uint8Array.byteLength) {
51
+ this.addIssue("error", "truncated_header", "段表在段大小信息之前结束", `header.segment[${i}]`, "消息文件可能已损坏");
52
+ return;
53
+ }
54
+ segmentSizes.push(view.getUint32(offset, true));
55
+ offset += 4;
56
+ }
57
+ offset = offset + 7 & -8;
58
+ if (offset > uint8Array.byteLength) {
59
+ this.addIssue("error", "truncated_header", "段表头部不完整", "header", "消息文件可能已损坏");
60
+ return;
61
+ }
62
+ let totalWords = 0;
63
+ for (let i = 0; i < segmentSizes.length; i++) {
64
+ const size = segmentSizes[i];
65
+ totalWords += size;
66
+ if (offset + size * WORD_SIZE > uint8Array.byteLength) {
67
+ this.addIssue("warning", "truncated_segment", `段${i}数据不足,声明大小:${size}字,实际可用:${Math.floor((uint8Array.byteLength - offset) / WORD_SIZE)}字`, `segment[${i}]`, "消息文件可能已损坏或被截断");
68
+ break;
69
+ }
70
+ const segmentBuffer = uint8Array.slice(offset, offset + size * WORD_SIZE);
71
+ this.segments.push(Segment.fromBuffer(segmentBuffer.buffer));
72
+ offset += size * WORD_SIZE;
73
+ }
74
+ if (totalWords > this.options.maxTotalSize / WORD_SIZE) this.addIssue("warning", "large_message", `消息总字数(${totalWords})较大,可能影响性能`, "message", "考虑分割大型消息");
75
+ this.scanAllPointers();
76
+ }
77
+ /**
78
+ * 添加审计问题
79
+ */
80
+ addIssue(type, category, message, location, suggestion) {
81
+ this.issues.push({
82
+ type,
83
+ category,
84
+ message,
85
+ location,
86
+ suggestion
87
+ });
88
+ }
89
+ /**
90
+ * 获取段
91
+ */
92
+ getSegment(index) {
93
+ return this.segments[index];
94
+ }
95
+ /**
96
+ * 扫描所有指针
97
+ */
98
+ scanAllPointers() {
99
+ if (this.segments.length === 0) return;
100
+ for (let segIdx = 0; segIdx < this.segments.length; segIdx++) {
101
+ const segment = this.segments[segIdx];
102
+ const wordCount = segment.wordCount;
103
+ for (let wordIdx = 0; wordIdx < wordCount; wordIdx++) {
104
+ const ptrValue = segment.getWord(wordIdx);
105
+ if (ptrValue === 0n) continue;
106
+ this.scanPointer(segIdx, wordIdx, ptrValue, 0);
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * 扫描单个指针
112
+ */
113
+ scanPointer(segmentIndex, wordOffset, ptrValue, depth) {
114
+ this.pointersScanned++;
115
+ this.maxNestingDepth = Math.max(this.maxNestingDepth, depth);
116
+ const ptrKey = `${segmentIndex}:${wordOffset}`;
117
+ if (this.visitedPointers.has(ptrKey)) {
118
+ this.addIssue("warning", "circular_reference", `检测到可能的循环引用 at segment[${segmentIndex}].word[${wordOffset}]`, `segment[${segmentIndex}].word[${wordOffset}]`, "检查消息结构是否存在循环");
119
+ return;
120
+ }
121
+ this.visitedPointers.add(ptrKey);
122
+ if (depth > 100) {
123
+ this.addIssue("error", "nesting_too_deep", "指针嵌套深度超过100,可能存在恶意构造的消息", `segment[${segmentIndex}].word[${wordOffset}]`, "检查消息是否被恶意构造");
124
+ return;
125
+ }
126
+ const ptr = decodePointer(ptrValue);
127
+ const segment = this.getSegment(segmentIndex);
128
+ if (!segment) return;
129
+ switch (ptr.tag) {
130
+ case PointerTag.STRUCT: {
131
+ const structPtr = ptr;
132
+ const targetOffset = wordOffset + 1 + structPtr.offset;
133
+ if (targetOffset < 0 || targetOffset + structPtr.dataWords + structPtr.pointerCount > segment.wordCount) this.addIssue("error", "out_of_bounds", `Struct指针目标超出段范围: offset=${structPtr.offset}, dataWords=${structPtr.dataWords}, pointerCount=${structPtr.pointerCount}`, `segment[${segmentIndex}].word[${wordOffset}]`, "消息可能已损坏或被篡改");
134
+ const pointerStart = targetOffset + structPtr.dataWords;
135
+ for (let i = 0; i < structPtr.pointerCount; i++) {
136
+ const ptrIdx = pointerStart + i;
137
+ if (ptrIdx < segment.wordCount) {
138
+ const nestedPtr = segment.getWord(ptrIdx);
139
+ if (nestedPtr !== 0n) this.scanPointer(segmentIndex, ptrIdx, nestedPtr, depth + 1);
140
+ }
141
+ }
142
+ break;
143
+ }
144
+ case PointerTag.LIST: {
145
+ const listPtr = ptr;
146
+ if (listPtr.elementSize === ElementSize.COMPOSITE) {
147
+ const tagOffset = wordOffset + 1 + listPtr.offset;
148
+ if (tagOffset >= 0 && tagOffset < segment.wordCount) {
149
+ const tagWord = segment.getWord(tagOffset);
150
+ const elementCount = Number(tagWord & BigInt(4294967295));
151
+ const dataWords = Number(tagWord >> BigInt(32) & BigInt(65535));
152
+ const pointerCount = Number(tagWord >> BigInt(48) & BigInt(65535));
153
+ if (elementCount > 1e6) this.addIssue("warning", "large_list", `复合列表元素数量异常: ${elementCount}`, `segment[${segmentIndex}].word[${wordOffset}]`, "检查列表大小是否合理");
154
+ const elementSize = dataWords + pointerCount;
155
+ const dataStart = tagOffset + 1;
156
+ for (let i = 0; i < elementCount; i++) for (let j = 0; j < pointerCount; j++) {
157
+ const ptrIdx = dataStart + i * elementSize + dataWords + j;
158
+ if (ptrIdx < segment.wordCount) {
159
+ const nestedPtr = segment.getWord(ptrIdx);
160
+ if (nestedPtr !== 0n) this.scanPointer(segmentIndex, ptrIdx, nestedPtr, depth + 1);
161
+ }
162
+ }
163
+ }
164
+ } else if (listPtr.elementSize === ElementSize.POINTER) {
165
+ const targetOffset = wordOffset + 1 + listPtr.offset;
166
+ for (let i = 0; i < listPtr.elementCount; i++) {
167
+ const ptrIdx = targetOffset + i;
168
+ if (ptrIdx < segment.wordCount) {
169
+ const nestedPtr = segment.getWord(ptrIdx);
170
+ if (nestedPtr !== 0n) this.scanPointer(segmentIndex, ptrIdx, nestedPtr, depth + 1);
171
+ }
172
+ }
173
+ }
174
+ break;
175
+ }
176
+ case PointerTag.FAR: {
177
+ const farPtr = ptr;
178
+ if (farPtr.targetSegment >= this.segments.length) this.addIssue("error", "invalid_far_pointer", `Far指针引用不存在的段: ${farPtr.targetSegment}`, `segment[${segmentIndex}].word[${wordOffset}]`, "消息可能已损坏");
179
+ else {
180
+ const targetSegment = this.getSegment(farPtr.targetSegment);
181
+ if (targetSegment && farPtr.targetOffset >= targetSegment.wordCount) this.addIssue("error", "invalid_far_pointer", `Far指针目标偏移超出范围: segment=${farPtr.targetSegment}, offset=${farPtr.targetOffset}`, `segment[${segmentIndex}].word[${wordOffset}]`, "消息可能已损坏");
182
+ if (farPtr.doubleFar) {
183
+ if (targetSegment && farPtr.targetOffset < targetSegment.wordCount) {
184
+ const landingPadPtr = targetSegment.getWord(farPtr.targetOffset);
185
+ if (landingPadPtr !== 0n) this.scanPointer(farPtr.targetSegment, farPtr.targetOffset, landingPadPtr, depth + 1);
186
+ }
187
+ }
188
+ }
189
+ break;
190
+ }
191
+ case PointerTag.OTHER:
192
+ this.addIssue("warning", "capability_in_message", "序列化消息中发现 Capability 或其他特殊指针", `segment[${segmentIndex}].word[${wordOffset}]`, "Capability 通常不应在序列化消息中");
193
+ break;
194
+ }
195
+ }
196
+ /**
197
+ * 生成审计报告
198
+ */
199
+ generateReport(filePath, fileSize) {
200
+ const errors = this.issues.filter((i) => i.type === "error").length;
201
+ const warnings = this.issues.filter((i) => i.type === "warning").length;
202
+ const infos = this.issues.filter((i) => i.type === "info").length;
203
+ return {
204
+ filePath,
205
+ fileSize,
206
+ passed: errors === 0,
207
+ segmentCount: this.segments.length,
208
+ totalWords: this.segments.reduce((sum, s) => sum + s.wordCount, 0),
209
+ issues: this.issues,
210
+ summary: {
211
+ error: errors,
212
+ warning: warnings,
213
+ info: infos
214
+ },
215
+ pointersScanned: this.pointersScanned,
216
+ maxNestingDepth: this.maxNestingDepth
217
+ };
218
+ }
219
+ };
220
+ /**
221
+ * 格式化审计报告为可读文本
222
+ */
223
+ function formatAuditReport(report) {
224
+ const lines = [];
225
+ lines.push("=".repeat(60));
226
+ lines.push("CAP'N PROTO SECURITY AUDIT REPORT");
227
+ lines.push("=".repeat(60));
228
+ lines.push("");
229
+ lines.push("File Information:");
230
+ lines.push(` Path: ${report.filePath}`);
231
+ lines.push(` Size: ${report.fileSize.toLocaleString()} bytes`);
232
+ lines.push(` Segments: ${report.segmentCount}`);
233
+ lines.push(` Total Words: ${report.totalWords.toLocaleString()}`);
234
+ lines.push(` Pointers Scanned: ${report.pointersScanned.toLocaleString()}`);
235
+ lines.push(` Max Nesting Depth: ${report.maxNestingDepth}`);
236
+ lines.push("");
237
+ if (report.passed) lines.push("✅ AUDIT PASSED");
238
+ else lines.push("❌ AUDIT FAILED");
239
+ lines.push("");
240
+ lines.push("Summary:");
241
+ lines.push(` Errors: ${report.summary.error}`);
242
+ lines.push(` Warnings: ${report.summary.warning}`);
243
+ lines.push(` Info: ${report.summary.info}`);
244
+ lines.push("");
245
+ if (report.issues.length > 0) {
246
+ lines.push("Issues:");
247
+ lines.push("-".repeat(60));
248
+ const errors = report.issues.filter((i) => i.type === "error");
249
+ const warnings = report.issues.filter((i) => i.type === "warning");
250
+ const infos = report.issues.filter((i) => i.type === "info");
251
+ if (errors.length > 0) {
252
+ lines.push("");
253
+ lines.push("❌ ERRORS:");
254
+ for (const issue of errors) {
255
+ lines.push(` [${issue.category}] ${issue.message}`);
256
+ lines.push(` Location: ${issue.location}`);
257
+ if (issue.suggestion) lines.push(` 💡 ${issue.suggestion}`);
258
+ lines.push("");
259
+ }
260
+ }
261
+ if (warnings.length > 0) {
262
+ lines.push("");
263
+ lines.push("⚠️ WARNINGS:");
264
+ for (const issue of warnings) {
265
+ lines.push(` [${issue.category}] ${issue.message}`);
266
+ lines.push(` Location: ${issue.location}`);
267
+ if (issue.suggestion) lines.push(` 💡 ${issue.suggestion}`);
268
+ lines.push("");
269
+ }
270
+ }
271
+ if (infos.length > 0) {
272
+ lines.push("");
273
+ lines.push("ℹ️ INFO:");
274
+ for (const issue of infos) {
275
+ lines.push(` [${issue.category}] ${issue.message}`);
276
+ lines.push(` Location: ${issue.location}`);
277
+ lines.push("");
278
+ }
279
+ }
280
+ } else lines.push("No issues found.");
281
+ lines.push("");
282
+ lines.push("=".repeat(60));
283
+ return lines.join("\n");
284
+ }
285
+ /**
286
+ * 审计消息文件
287
+ */
288
+ function auditMessageFile(filePath, options = {}) {
289
+ const buffer = readFileSync(filePath);
290
+ return new AuditReader(buffer, options).generateReport(filePath, buffer.byteLength);
291
+ }
292
+ const CLI_VERSION = "0.9.0";
293
+ function printUsage() {
294
+ console.log(`
295
+ Cap'n Proto Security Audit CLI v${CLI_VERSION}
296
+
297
+ Usage: capnp audit <file> [options]
298
+
299
+ Arguments:
300
+ file Path to Cap'n Proto message binary file
301
+
302
+ Options:
303
+ --strict Strict mode, fail on warnings
304
+ --json Output as JSON
305
+ -o, --output <file> Write report to file
306
+ --max-segments <n> Maximum allowed segments (default: 64)
307
+ --max-size <bytes> Maximum message size in bytes (default: 64MB)
308
+ -h, --help Show this help
309
+
310
+ Examples:
311
+ capnp audit message.bin
312
+ capnp audit message.bin --strict
313
+ capnp audit message.bin --json -o report.json
314
+ capnp audit message.bin --max-segments 128 --max-size 134217728
315
+ `);
316
+ }
317
+ function parseArgs(args) {
318
+ const options = {};
319
+ for (let i = 0; i < args.length; i++) {
320
+ const arg = args[i];
321
+ if (arg === "-h" || arg === "--help") {
322
+ printUsage();
323
+ process.exit(0);
324
+ }
325
+ if (arg === "--strict") options.strict = true;
326
+ else if (arg === "--json") options.json = true;
327
+ else if (arg === "-o" || arg === "--output") options.output = args[++i];
328
+ else if (arg === "--max-segments") {
329
+ const val = Number.parseInt(args[++i], 10);
330
+ if (Number.isNaN(val) || val <= 0) {
331
+ console.error("Error: --max-segments must be a positive integer");
332
+ process.exit(1);
333
+ }
334
+ options.maxSegments = val;
335
+ } else if (arg === "--max-size") {
336
+ const val = Number.parseInt(args[++i], 10);
337
+ if (Number.isNaN(val) || val <= 0) {
338
+ console.error("Error: --max-size must be a positive integer");
339
+ process.exit(1);
340
+ }
341
+ options.maxSize = val;
342
+ } else if (!arg.startsWith("-")) {
343
+ if (!options.file) options.file = arg;
344
+ }
345
+ }
346
+ return options;
347
+ }
348
+ async function run(args) {
349
+ const options = parseArgs(args);
350
+ if (!options.file) {
351
+ console.error("Error: File path is required");
352
+ printUsage();
353
+ process.exit(1);
354
+ }
355
+ try {
356
+ const auditOptions = {
357
+ strictMode: options.strict,
358
+ maxSegments: options.maxSegments,
359
+ maxTotalSize: options.maxSize
360
+ };
361
+ const report = auditMessageFile(options.file, auditOptions);
362
+ let output;
363
+ if (options.json) output = JSON.stringify(report, null, 2);
364
+ else output = formatAuditReport(report);
365
+ if (options.output) {
366
+ const { writeFileSync } = await import("node:fs");
367
+ writeFileSync(options.output, output, "utf-8");
368
+ console.log(`Report written to: ${options.output}`);
369
+ } else console.log(output);
370
+ const shouldFail = !report.passed || options.strict && report.summary.warning > 0;
371
+ process.exit(shouldFail ? 1 : 0);
372
+ } catch (err) {
373
+ console.error("Error:", err instanceof Error ? err.message : err);
374
+ process.exit(2);
375
+ }
376
+ }
377
+
378
+ //#endregion
379
+ export { run };
380
+ //# sourceMappingURL=cli-audit-j58Eyd5d.js.map