@circuit-forge/eda-core 1.0.0
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/erc/checker.d.ts +15 -0
- package/dist/erc/checker.d.ts.map +1 -0
- package/dist/erc/checker.js +267 -0
- package/dist/erc/checker.js.map +1 -0
- package/dist/erc/codes.d.ts +15 -0
- package/dist/erc/codes.d.ts.map +1 -0
- package/dist/erc/codes.js +48 -0
- package/dist/erc/codes.js.map +1 -0
- package/dist/erc/index.d.ts +7 -0
- package/dist/erc/index.d.ts.map +1 -0
- package/dist/erc/index.js +14 -0
- package/dist/erc/index.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/netlist/generator.d.ts +32 -0
- package/dist/netlist/generator.d.ts.map +1 -0
- package/dist/netlist/generator.js +183 -0
- package/dist/netlist/generator.js.map +1 -0
- package/dist/netlist/index.d.ts +6 -0
- package/dist/netlist/index.d.ts.map +1 -0
- package/dist/netlist/index.js +20 -0
- package/dist/netlist/index.js.map +1 -0
- package/dist/netlist/sanitizer.d.ts +45 -0
- package/dist/netlist/sanitizer.d.ts.map +1 -0
- package/dist/netlist/sanitizer.js +145 -0
- package/dist/netlist/sanitizer.js.map +1 -0
- package/dist/parser/csv-parser.d.ts +27 -0
- package/dist/parser/csv-parser.d.ts.map +1 -0
- package/dist/parser/csv-parser.js +198 -0
- package/dist/parser/csv-parser.js.map +1 -0
- package/dist/parser/index.d.ts +6 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +15 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/netlist-parser.d.ts +25 -0
- package/dist/parser/netlist-parser.d.ts.map +1 -0
- package/dist/parser/netlist-parser.js +260 -0
- package/dist/parser/netlist-parser.js.map +1 -0
- package/dist/schemas/analysis.schema.d.ts +298 -0
- package/dist/schemas/analysis.schema.d.ts.map +1 -0
- package/dist/schemas/analysis.schema.js +91 -0
- package/dist/schemas/analysis.schema.js.map +1 -0
- package/dist/schemas/circuit.schema.d.ts +405 -0
- package/dist/schemas/circuit.schema.d.ts.map +1 -0
- package/dist/schemas/circuit.schema.js +121 -0
- package/dist/schemas/circuit.schema.js.map +1 -0
- package/dist/types/analysis.d.ts +61 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +119 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/circuit.d.ts +94 -0
- package/dist/types/circuit.d.ts.map +1 -0
- package/dist/types/circuit.js +32 -0
- package/dist/types/circuit.js.map +1 -0
- package/dist/types/erc.d.ts +68 -0
- package/dist/types/erc.d.ts.map +1 -0
- package/dist/types/erc.js +33 -0
- package/dist/types/erc.js.map +1 -0
- package/dist/types/simulation.d.ts +61 -0
- package/dist/types/simulation.d.ts.map +1 -0
- package/dist/types/simulation.js +43 -0
- package/dist/types/simulation.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +14 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/unit-parser.d.ts +47 -0
- package/dist/utils/unit-parser.d.ts.map +1 -0
- package/dist/utils/unit-parser.js +229 -0
- package/dist/utils/unit-parser.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateNetlist = generateNetlist;
|
|
4
|
+
exports.getNodeNames = getNodeNames;
|
|
5
|
+
exports.validateNetlist = validateNetlist;
|
|
6
|
+
const circuit_1 = require("../types/circuit");
|
|
7
|
+
const analysis_1 = require("../types/analysis");
|
|
8
|
+
const sanitizer_1 = require("./sanitizer");
|
|
9
|
+
/**
|
|
10
|
+
* Default diode model for circuits without explicit model
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_DIODE_MODEL = `.model DDEFAULT D(IS=1e-14 N=1.05 RS=10 BV=100 IBV=1e-10)`;
|
|
13
|
+
/**
|
|
14
|
+
* Generate a complete SPICE netlist from circuit JSON
|
|
15
|
+
*/
|
|
16
|
+
function generateNetlist(circuit, analysis, options = {}) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
// Title
|
|
19
|
+
const title = options.title || circuit.metadata?.name || 'Untitled Circuit';
|
|
20
|
+
lines.push(`* ${title}`);
|
|
21
|
+
lines.push(`* Generated by eda-core`);
|
|
22
|
+
lines.push(`* ${new Date().toISOString()}`);
|
|
23
|
+
lines.push('');
|
|
24
|
+
// Build node map
|
|
25
|
+
const nodeMap = buildNodeMap(circuit.nets);
|
|
26
|
+
// Check if we need default diode model
|
|
27
|
+
const needsDiodeModel = circuit.components.some((c) => c.type === 'diode' && !c.model);
|
|
28
|
+
if (needsDiodeModel) {
|
|
29
|
+
lines.push('* Default diode model');
|
|
30
|
+
lines.push(DEFAULT_DIODE_MODEL);
|
|
31
|
+
lines.push('');
|
|
32
|
+
}
|
|
33
|
+
// Include files (with validation)
|
|
34
|
+
if (options.includeFiles && options.includeFiles.length > 0) {
|
|
35
|
+
if (options.jobDir) {
|
|
36
|
+
(0, sanitizer_1.validateIncludePaths)(options.includeFiles, options.jobDir);
|
|
37
|
+
}
|
|
38
|
+
lines.push('* Include files');
|
|
39
|
+
for (const file of options.includeFiles) {
|
|
40
|
+
lines.push(`.include "${file}"`);
|
|
41
|
+
}
|
|
42
|
+
lines.push('');
|
|
43
|
+
}
|
|
44
|
+
// Components
|
|
45
|
+
lines.push('* Components');
|
|
46
|
+
for (const component of circuit.components) {
|
|
47
|
+
const spiceLine = componentToSpice(component, nodeMap);
|
|
48
|
+
if (spiceLine) {
|
|
49
|
+
lines.push(spiceLine);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push('');
|
|
53
|
+
// Analysis
|
|
54
|
+
lines.push('* Analysis');
|
|
55
|
+
lines.push((0, analysis_1.analysisToSpice)(analysis));
|
|
56
|
+
lines.push('');
|
|
57
|
+
// Control block for output
|
|
58
|
+
const probes = options.probes || generateDefaultProbes(circuit, nodeMap);
|
|
59
|
+
lines.push('* Control block');
|
|
60
|
+
lines.push('.control');
|
|
61
|
+
lines.push(' set filetype=ascii');
|
|
62
|
+
lines.push(' run');
|
|
63
|
+
if (probes.length > 0) {
|
|
64
|
+
const probeList = probes.join(' ');
|
|
65
|
+
lines.push(` wrdata output.csv ${probeList}`);
|
|
66
|
+
}
|
|
67
|
+
lines.push(' quit');
|
|
68
|
+
lines.push('.endc');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push('.end');
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a map from net IDs to SPICE node names
|
|
75
|
+
*/
|
|
76
|
+
function buildNodeMap(nets) {
|
|
77
|
+
const nodeMap = new Map();
|
|
78
|
+
for (const net of nets) {
|
|
79
|
+
if (net.isGround) {
|
|
80
|
+
nodeMap.set(net.id, '0');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
nodeMap.set(net.id, (0, sanitizer_1.sanitizeNodeName)(net.id));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return nodeMap;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Convert a component to a SPICE line
|
|
90
|
+
*/
|
|
91
|
+
function componentToSpice(component, nodeMap) {
|
|
92
|
+
const { type, designator, value, model, pins } = component;
|
|
93
|
+
// Ground is not a real component
|
|
94
|
+
if (type === 'ground') {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const prefix = circuit_1.SPICE_PREFIXES[type];
|
|
98
|
+
if (!prefix) {
|
|
99
|
+
throw new Error(`Unknown component type: ${type}`);
|
|
100
|
+
}
|
|
101
|
+
// Get node names for pins
|
|
102
|
+
const nodes = pins.map((pin) => {
|
|
103
|
+
const node = nodeMap.get(pin.netId);
|
|
104
|
+
if (!node) {
|
|
105
|
+
throw new Error(`Net not found: ${pin.netId} for component ${designator}`);
|
|
106
|
+
}
|
|
107
|
+
return node;
|
|
108
|
+
});
|
|
109
|
+
// Generate SPICE line based on component type
|
|
110
|
+
switch (type) {
|
|
111
|
+
case 'resistor':
|
|
112
|
+
case 'capacitor':
|
|
113
|
+
case 'inductor':
|
|
114
|
+
return `${designator} ${nodes.join(' ')} ${value || '0'}`;
|
|
115
|
+
case 'voltage_source':
|
|
116
|
+
case 'current_source':
|
|
117
|
+
return `${designator} ${nodes.join(' ')} ${value || 'DC 0'}`;
|
|
118
|
+
case 'diode':
|
|
119
|
+
return `${designator} ${nodes.join(' ')} ${model || 'DDEFAULT'}`;
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unsupported component type: ${type}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generate default probes (all node voltages)
|
|
126
|
+
*/
|
|
127
|
+
function generateDefaultProbes(circuit, nodeMap) {
|
|
128
|
+
const probes = [];
|
|
129
|
+
for (const net of circuit.nets) {
|
|
130
|
+
if (!net.isGround) {
|
|
131
|
+
const nodeName = nodeMap.get(net.id);
|
|
132
|
+
if (nodeName) {
|
|
133
|
+
probes.push(`v(${nodeName})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return probes;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get all unique node names from a circuit
|
|
141
|
+
*/
|
|
142
|
+
function getNodeNames(circuit) {
|
|
143
|
+
const nodeMap = buildNodeMap(circuit.nets);
|
|
144
|
+
return Array.from(nodeMap.values());
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Validate that a netlist is syntactically correct (basic check)
|
|
148
|
+
*/
|
|
149
|
+
function validateNetlist(netlist) {
|
|
150
|
+
const errors = [];
|
|
151
|
+
const lines = netlist.split('\n');
|
|
152
|
+
let hasEnd = false;
|
|
153
|
+
let hasAnalysis = false;
|
|
154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
155
|
+
const line = lines[i]?.trim() || '';
|
|
156
|
+
// Skip comments and empty lines
|
|
157
|
+
if (line.startsWith('*') || line === '') {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Check for .end
|
|
161
|
+
if (line.toLowerCase() === '.end') {
|
|
162
|
+
hasEnd = true;
|
|
163
|
+
}
|
|
164
|
+
// Check for analysis
|
|
165
|
+
if (line.toLowerCase().startsWith('.tran') ||
|
|
166
|
+
line.toLowerCase().startsWith('.ac') ||
|
|
167
|
+
line.toLowerCase().startsWith('.dc') ||
|
|
168
|
+
line.toLowerCase().startsWith('.op')) {
|
|
169
|
+
hasAnalysis = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!hasEnd) {
|
|
173
|
+
errors.push('Netlist missing .end statement');
|
|
174
|
+
}
|
|
175
|
+
if (!hasAnalysis) {
|
|
176
|
+
errors.push('Netlist missing analysis command (.tran, .ac, .dc, or .op)');
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
valid: errors.length === 0,
|
|
180
|
+
errors,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/netlist/generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlist Module Exports
|
|
3
|
+
*/
|
|
4
|
+
export { generateNetlist, getNodeNames, validateNetlist, type NetlistOptions, } from './generator';
|
|
5
|
+
export { sanitizeNodeName, validateIncludePaths, validateIncludePath, sanitizeNetlist, sanitizeValue, validateDesignator, hasShellMetacharacters, SecurityError, } from './sanitizer';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/netlist/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACH,eAAe,EACf,YAAY,EACZ,eAAe,EACf,KAAK,cAAc,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACH,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,GAChB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecurityError = exports.hasShellMetacharacters = exports.validateDesignator = exports.sanitizeValue = exports.sanitizeNetlist = exports.validateIncludePath = exports.validateIncludePaths = exports.sanitizeNodeName = exports.validateNetlist = exports.getNodeNames = exports.generateNetlist = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Netlist Module Exports
|
|
6
|
+
*/
|
|
7
|
+
var generator_1 = require("./generator");
|
|
8
|
+
Object.defineProperty(exports, "generateNetlist", { enumerable: true, get: function () { return generator_1.generateNetlist; } });
|
|
9
|
+
Object.defineProperty(exports, "getNodeNames", { enumerable: true, get: function () { return generator_1.getNodeNames; } });
|
|
10
|
+
Object.defineProperty(exports, "validateNetlist", { enumerable: true, get: function () { return generator_1.validateNetlist; } });
|
|
11
|
+
var sanitizer_1 = require("./sanitizer");
|
|
12
|
+
Object.defineProperty(exports, "sanitizeNodeName", { enumerable: true, get: function () { return sanitizer_1.sanitizeNodeName; } });
|
|
13
|
+
Object.defineProperty(exports, "validateIncludePaths", { enumerable: true, get: function () { return sanitizer_1.validateIncludePaths; } });
|
|
14
|
+
Object.defineProperty(exports, "validateIncludePath", { enumerable: true, get: function () { return sanitizer_1.validateIncludePath; } });
|
|
15
|
+
Object.defineProperty(exports, "sanitizeNetlist", { enumerable: true, get: function () { return sanitizer_1.sanitizeNetlist; } });
|
|
16
|
+
Object.defineProperty(exports, "sanitizeValue", { enumerable: true, get: function () { return sanitizer_1.sanitizeValue; } });
|
|
17
|
+
Object.defineProperty(exports, "validateDesignator", { enumerable: true, get: function () { return sanitizer_1.validateDesignator; } });
|
|
18
|
+
Object.defineProperty(exports, "hasShellMetacharacters", { enumerable: true, get: function () { return sanitizer_1.hasShellMetacharacters; } });
|
|
19
|
+
Object.defineProperty(exports, "SecurityError", { enumerable: true, get: function () { return sanitizer_1.SecurityError; } });
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/netlist/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,yCAKqB;AAJjB,4GAAA,eAAe,OAAA;AACf,yGAAA,YAAY,OAAA;AACZ,4GAAA,eAAe,OAAA;AAGnB,yCASqB;AARjB,6GAAA,gBAAgB,OAAA;AAChB,iHAAA,oBAAoB,OAAA;AACpB,gHAAA,mBAAmB,OAAA;AACnB,4GAAA,eAAe,OAAA;AACf,0GAAA,aAAa,OAAA;AACb,+GAAA,kBAAkB,OAAA;AAClB,mHAAA,sBAAsB,OAAA;AACtB,0GAAA,aAAa,OAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlist Sanitization Utilities
|
|
3
|
+
* Security-focused input validation and path sanitization
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize a node name for SPICE compatibility
|
|
7
|
+
* - Removes special characters
|
|
8
|
+
* - Ensures it doesn't start with a number
|
|
9
|
+
* - Avoids reserved words
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeNodeName(name: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Validate include paths to prevent path traversal attacks
|
|
14
|
+
* All paths must be relative and within the job directory
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateIncludePaths(paths: string[], jobDir: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Validate a single include path
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateIncludePath(includePath: string, _jobDir: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize netlist content for dangerous patterns
|
|
23
|
+
*/
|
|
24
|
+
export declare function sanitizeNetlist(netlist: string, jobDir: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Security error class
|
|
27
|
+
*/
|
|
28
|
+
export declare class SecurityError extends Error {
|
|
29
|
+
readonly code: string;
|
|
30
|
+
constructor(message: string, code: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sanitize a component value string
|
|
34
|
+
* Removes potentially dangerous characters while keeping valid SPICE syntax
|
|
35
|
+
*/
|
|
36
|
+
export declare function sanitizeValue(value: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Validate a designator format
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateDesignator(designator: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Check if a string contains shell metacharacters
|
|
43
|
+
*/
|
|
44
|
+
export declare function hasShellMetacharacters(str: string): boolean;
|
|
45
|
+
//# sourceMappingURL=sanitizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitizer.d.ts","sourceRoot":"","sources":["../../src/netlist/sanitizer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyBrD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAI1E;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAgC9E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA2BvE;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACpC,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAK5C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAInD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAI3D"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Netlist Sanitization Utilities
|
|
4
|
+
* Security-focused input validation and path sanitization
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SecurityError = void 0;
|
|
8
|
+
exports.sanitizeNodeName = sanitizeNodeName;
|
|
9
|
+
exports.validateIncludePaths = validateIncludePaths;
|
|
10
|
+
exports.validateIncludePath = validateIncludePath;
|
|
11
|
+
exports.sanitizeNetlist = sanitizeNetlist;
|
|
12
|
+
exports.sanitizeValue = sanitizeValue;
|
|
13
|
+
exports.validateDesignator = validateDesignator;
|
|
14
|
+
exports.hasShellMetacharacters = hasShellMetacharacters;
|
|
15
|
+
/**
|
|
16
|
+
* Reserved SPICE words that cannot be used as node names
|
|
17
|
+
*/
|
|
18
|
+
const RESERVED_WORDS = new Set([
|
|
19
|
+
'all',
|
|
20
|
+
'none',
|
|
21
|
+
'in',
|
|
22
|
+
'out',
|
|
23
|
+
'vcc',
|
|
24
|
+
'vdd',
|
|
25
|
+
'vss',
|
|
26
|
+
'gnd',
|
|
27
|
+
'ground',
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Sanitize a node name for SPICE compatibility
|
|
31
|
+
* - Removes special characters
|
|
32
|
+
* - Ensures it doesn't start with a number
|
|
33
|
+
* - Avoids reserved words
|
|
34
|
+
*/
|
|
35
|
+
function sanitizeNodeName(name) {
|
|
36
|
+
// Remove special characters, keep alphanumeric and underscore
|
|
37
|
+
let sanitized = name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
38
|
+
// Ensure it doesn't start with a number
|
|
39
|
+
if (/^[0-9]/.test(sanitized)) {
|
|
40
|
+
sanitized = `n${sanitized}`;
|
|
41
|
+
}
|
|
42
|
+
// Prefix if it's a reserved word
|
|
43
|
+
if (RESERVED_WORDS.has(sanitized.toLowerCase())) {
|
|
44
|
+
sanitized = `x_${sanitized}`;
|
|
45
|
+
}
|
|
46
|
+
// Ensure not empty
|
|
47
|
+
if (sanitized === '') {
|
|
48
|
+
sanitized = 'node';
|
|
49
|
+
}
|
|
50
|
+
// Prefix with 'n' for clarity
|
|
51
|
+
if (!sanitized.startsWith('n') && !sanitized.startsWith('x_')) {
|
|
52
|
+
sanitized = `n${sanitized}`;
|
|
53
|
+
}
|
|
54
|
+
return sanitized;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate include paths to prevent path traversal attacks
|
|
58
|
+
* All paths must be relative and within the job directory
|
|
59
|
+
*/
|
|
60
|
+
function validateIncludePaths(paths, jobDir) {
|
|
61
|
+
for (const includePath of paths) {
|
|
62
|
+
validateIncludePath(includePath, jobDir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate a single include path
|
|
67
|
+
*/
|
|
68
|
+
function validateIncludePath(includePath, _jobDir) {
|
|
69
|
+
// Reject absolute paths (Unix or Windows style)
|
|
70
|
+
if (includePath.startsWith('/') || /^[A-Za-z]:/.test(includePath)) {
|
|
71
|
+
throw new SecurityError(`Absolute include path not allowed: ${includePath}`, 'ABSOLUTE_PATH');
|
|
72
|
+
}
|
|
73
|
+
// Reject path traversal
|
|
74
|
+
if (includePath.includes('..')) {
|
|
75
|
+
throw new SecurityError(`Path traversal not allowed: ${includePath}`, 'PATH_TRAVERSAL');
|
|
76
|
+
}
|
|
77
|
+
// Reject paths starting with special characters
|
|
78
|
+
if (includePath.startsWith('~') || includePath.startsWith('$')) {
|
|
79
|
+
throw new SecurityError(`Special path prefix not allowed: ${includePath}`, 'SPECIAL_PREFIX');
|
|
80
|
+
}
|
|
81
|
+
// Validate characters (alphanumeric, underscore, dash, dot, slash)
|
|
82
|
+
if (!/^[a-zA-Z0-9_\-./]+$/.test(includePath)) {
|
|
83
|
+
throw new SecurityError(`Invalid characters in include path: ${includePath}`, 'INVALID_CHARS');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize netlist content for dangerous patterns
|
|
88
|
+
*/
|
|
89
|
+
function sanitizeNetlist(netlist, jobDir) {
|
|
90
|
+
const lines = netlist.split('\n');
|
|
91
|
+
const sanitized = [];
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const trimmed = line.trim().toLowerCase();
|
|
94
|
+
// Validate .include statements
|
|
95
|
+
if (trimmed.startsWith('.include')) {
|
|
96
|
+
const match = line.match(/\.include\s+["']?([^"'\s]+)["']?/i);
|
|
97
|
+
if (match && match[1]) {
|
|
98
|
+
validateIncludePath(match[1], jobDir);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Block potentially dangerous directives
|
|
102
|
+
if (trimmed.startsWith('.shell') || trimmed.startsWith('.system')) {
|
|
103
|
+
throw new SecurityError('Shell commands not allowed in netlist', 'SHELL_COMMAND');
|
|
104
|
+
}
|
|
105
|
+
sanitized.push(line);
|
|
106
|
+
}
|
|
107
|
+
return sanitized.join('\n');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Security error class
|
|
111
|
+
*/
|
|
112
|
+
class SecurityError extends Error {
|
|
113
|
+
code;
|
|
114
|
+
constructor(message, code) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = 'SecurityError';
|
|
117
|
+
this.code = code;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.SecurityError = SecurityError;
|
|
121
|
+
/**
|
|
122
|
+
* Sanitize a component value string
|
|
123
|
+
* Removes potentially dangerous characters while keeping valid SPICE syntax
|
|
124
|
+
*/
|
|
125
|
+
function sanitizeValue(value) {
|
|
126
|
+
// Allow: numbers, letters (for units), spaces, parentheses (for sources), +/-/.
|
|
127
|
+
const sanitized = value.replace(/[^a-zA-Z0-9\s()+\-.,_]/g, '');
|
|
128
|
+
return sanitized.trim();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validate a designator format
|
|
132
|
+
*/
|
|
133
|
+
function validateDesignator(designator) {
|
|
134
|
+
// Must start with a letter, followed by alphanumeric, ending with a number
|
|
135
|
+
return /^[A-Za-z][A-Za-z0-9]*[0-9]+$/.test(designator);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if a string contains shell metacharacters
|
|
139
|
+
*/
|
|
140
|
+
function hasShellMetacharacters(str) {
|
|
141
|
+
// Characters that could be used for shell injection
|
|
142
|
+
const shellMeta = /[;&|`$<>\\!#{}[\]*?'"]/;
|
|
143
|
+
return shellMeta.test(str);
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=sanitizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitizer.js","sourceRoot":"","sources":["../../src/netlist/sanitizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV Parser for ngspice wrdata output
|
|
3
|
+
* Parses simulation output into structured JSON format
|
|
4
|
+
*/
|
|
5
|
+
import type { SimulationResult } from '../types/simulation';
|
|
6
|
+
/**
|
|
7
|
+
* Parse ngspice wrdata CSV output to SimulationResult
|
|
8
|
+
*
|
|
9
|
+
* wrdata format:
|
|
10
|
+
* - Whitespace-separated values
|
|
11
|
+
* - First column is X axis (time, frequency, voltage depending on analysis)
|
|
12
|
+
* - Subsequent columns are Y values for each probe
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseCsv(csvContent: string, probeNames: string[], analysisType?: string): SimulationResult;
|
|
15
|
+
/**
|
|
16
|
+
* Parse ngspice raw ASCII format (alternative parser)
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseRawAscii(rawContent: string, analysisType?: string): SimulationResult;
|
|
19
|
+
/**
|
|
20
|
+
* Detect output format from content
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectOutputFormat(content: string): 'csv' | 'raw' | 'unknown';
|
|
23
|
+
/**
|
|
24
|
+
* Auto-parse output based on detected format
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseSimulationOutput(content: string, probeNames: string[], analysisType?: string): SimulationResult;
|
|
27
|
+
//# sourceMappingURL=csv-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv-parser.d.ts","sourceRoot":"","sources":["../../src/parser/csv-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAqC,MAAM,qBAAqB,CAAC;AAE/F;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAAE,EACpB,YAAY,GAAE,MAAe,GAC9B,gBAAgB,CA6ClB;AAoDD;;GAEG;AACH,wBAAgB,aAAa,CACzB,UAAU,EAAE,MAAM,EAClB,YAAY,GAAE,MAAe,GAC9B,gBAAgB,CAqElB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,CAkB7E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,EACpB,YAAY,GAAE,MAAe,GAC9B,gBAAgB,CAWlB"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCsv = parseCsv;
|
|
4
|
+
exports.parseRawAscii = parseRawAscii;
|
|
5
|
+
exports.detectOutputFormat = detectOutputFormat;
|
|
6
|
+
exports.parseSimulationOutput = parseSimulationOutput;
|
|
7
|
+
/**
|
|
8
|
+
* Parse ngspice wrdata CSV output to SimulationResult
|
|
9
|
+
*
|
|
10
|
+
* wrdata format:
|
|
11
|
+
* - Whitespace-separated values
|
|
12
|
+
* - First column is X axis (time, frequency, voltage depending on analysis)
|
|
13
|
+
* - Subsequent columns are Y values for each probe
|
|
14
|
+
*/
|
|
15
|
+
function parseCsv(csvContent, probeNames, analysisType = 'tran') {
|
|
16
|
+
const lines = csvContent.trim().split('\n');
|
|
17
|
+
if (lines.length === 0) {
|
|
18
|
+
return createEmptyResult(analysisType);
|
|
19
|
+
}
|
|
20
|
+
// Initialize series for each probe
|
|
21
|
+
const series = probeNames.map((name) => ({
|
|
22
|
+
name,
|
|
23
|
+
points: [],
|
|
24
|
+
}));
|
|
25
|
+
// Parse each line.
|
|
26
|
+
// ngspice `wrdata` (ascii) repeats the scale (x) column BEFORE each vector, so a row
|
|
27
|
+
// for N probes is laid out as: x y1 x y2 x y3 ... (2 columns per probe). We detect
|
|
28
|
+
// that interleaved layout by width and fall back to a single leading x column
|
|
29
|
+
// (x y1 y2 y3 ...) for other producers.
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('*')) {
|
|
33
|
+
continue; // Skip empty lines and comments
|
|
34
|
+
}
|
|
35
|
+
const values = trimmed.split(/\s+/).map(parseFloat);
|
|
36
|
+
if (values.length < 2) {
|
|
37
|
+
continue; // Need at least X and one Y value
|
|
38
|
+
}
|
|
39
|
+
const interleaved = values.length >= 2 * probeNames.length;
|
|
40
|
+
for (let i = 0; i < probeNames.length; i++) {
|
|
41
|
+
const x = interleaved ? values[2 * i] : values[0];
|
|
42
|
+
const y = interleaved ? values[2 * i + 1] : values[i + 1];
|
|
43
|
+
if (x !== undefined && y !== undefined && !isNaN(x) && !isNaN(y)) {
|
|
44
|
+
series[i]?.points.push({ x, y });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Build metadata
|
|
49
|
+
const meta = buildMeta(analysisType, series);
|
|
50
|
+
return { meta, series };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build result metadata
|
|
54
|
+
*/
|
|
55
|
+
function buildMeta(analysisType, series) {
|
|
56
|
+
const pointsCount = series[0]?.points.length || 0;
|
|
57
|
+
let xLabel = 'time';
|
|
58
|
+
let xUnit = 's';
|
|
59
|
+
switch (analysisType) {
|
|
60
|
+
case 'tran':
|
|
61
|
+
xLabel = 'time';
|
|
62
|
+
xUnit = 's';
|
|
63
|
+
break;
|
|
64
|
+
case 'ac':
|
|
65
|
+
xLabel = 'frequency';
|
|
66
|
+
xUnit = 'Hz';
|
|
67
|
+
break;
|
|
68
|
+
case 'dc':
|
|
69
|
+
xLabel = 'voltage';
|
|
70
|
+
xUnit = 'V';
|
|
71
|
+
break;
|
|
72
|
+
case 'op':
|
|
73
|
+
xLabel = 'point';
|
|
74
|
+
xUnit = undefined;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
analysisType,
|
|
79
|
+
xLabel,
|
|
80
|
+
xUnit,
|
|
81
|
+
pointsCount,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create an empty result
|
|
86
|
+
*/
|
|
87
|
+
function createEmptyResult(analysisType) {
|
|
88
|
+
return {
|
|
89
|
+
meta: {
|
|
90
|
+
analysisType,
|
|
91
|
+
xLabel: 'time',
|
|
92
|
+
pointsCount: 0,
|
|
93
|
+
},
|
|
94
|
+
series: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse ngspice raw ASCII format (alternative parser)
|
|
99
|
+
*/
|
|
100
|
+
function parseRawAscii(rawContent, analysisType = 'tran') {
|
|
101
|
+
const lines = rawContent.split('\n');
|
|
102
|
+
let inData = false;
|
|
103
|
+
let variables = [];
|
|
104
|
+
const dataBlocks = [];
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
const trimmed = line.trim();
|
|
107
|
+
// Parse header
|
|
108
|
+
if (trimmed.startsWith('No. Variables:')) {
|
|
109
|
+
// Number of variables
|
|
110
|
+
}
|
|
111
|
+
else if (trimmed.startsWith('No. Points:')) {
|
|
112
|
+
// Parse number of points (for reference, not used directly)
|
|
113
|
+
}
|
|
114
|
+
else if (trimmed.startsWith('Variables:')) {
|
|
115
|
+
inData = false;
|
|
116
|
+
// Next lines will be variable names
|
|
117
|
+
}
|
|
118
|
+
else if (trimmed.match(/^\d+\s+[\w()]+/)) {
|
|
119
|
+
// Variable definition line: "0 time time"
|
|
120
|
+
const parts = trimmed.split(/\s+/);
|
|
121
|
+
if (parts[1]) {
|
|
122
|
+
variables.push(parts[1]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (trimmed === 'Values:' || trimmed === 'Binary:') {
|
|
126
|
+
inData = true;
|
|
127
|
+
}
|
|
128
|
+
else if (inData && trimmed !== '') {
|
|
129
|
+
// Parse data values
|
|
130
|
+
const values = trimmed.split(/\s+/).map((v) => {
|
|
131
|
+
// Handle complex numbers (take real part)
|
|
132
|
+
if (v.includes(',')) {
|
|
133
|
+
return parseFloat(v.split(',')[0] || '0');
|
|
134
|
+
}
|
|
135
|
+
return parseFloat(v);
|
|
136
|
+
});
|
|
137
|
+
if (values.length > 0 && !values.some(isNaN)) {
|
|
138
|
+
dataBlocks.push(values);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Convert to series format
|
|
143
|
+
const series = [];
|
|
144
|
+
if (variables.length > 1 && dataBlocks.length > 0) {
|
|
145
|
+
// First variable is usually time/frequency
|
|
146
|
+
for (let i = 1; i < variables.length; i++) {
|
|
147
|
+
const points = [];
|
|
148
|
+
for (const block of dataBlocks) {
|
|
149
|
+
const x = block[0];
|
|
150
|
+
const y = block[i];
|
|
151
|
+
if (x !== undefined && y !== undefined) {
|
|
152
|
+
points.push({ x, y });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
series.push({
|
|
156
|
+
name: variables[i] || `var${i}`,
|
|
157
|
+
points,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
meta: buildMeta(analysisType, series),
|
|
163
|
+
series,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Detect output format from content
|
|
168
|
+
*/
|
|
169
|
+
function detectOutputFormat(content) {
|
|
170
|
+
const firstLines = content.split('\n').slice(0, 10).join('\n');
|
|
171
|
+
if (firstLines.includes('Plotname:') || firstLines.includes('No. Variables:')) {
|
|
172
|
+
return 'raw';
|
|
173
|
+
}
|
|
174
|
+
// CSV-like format (just numbers separated by whitespace)
|
|
175
|
+
const firstDataLine = content.split('\n').find((l) => {
|
|
176
|
+
const trimmed = l.trim();
|
|
177
|
+
return trimmed !== '' && !trimmed.startsWith('*') && !trimmed.startsWith('#');
|
|
178
|
+
});
|
|
179
|
+
if (firstDataLine && /^[\d.eE+\-\s]+$/.test(firstDataLine.trim())) {
|
|
180
|
+
return 'csv';
|
|
181
|
+
}
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Auto-parse output based on detected format
|
|
186
|
+
*/
|
|
187
|
+
function parseSimulationOutput(content, probeNames, analysisType = 'tran') {
|
|
188
|
+
const format = detectOutputFormat(content);
|
|
189
|
+
switch (format) {
|
|
190
|
+
case 'csv':
|
|
191
|
+
return parseCsv(content, probeNames, analysisType);
|
|
192
|
+
case 'raw':
|
|
193
|
+
return parseRawAscii(content, analysisType);
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`Unknown output format. Content starts with: ${content.substring(0, 100)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=csv-parser.js.map
|