@portll/cobolwork 0.6.0 → 0.7.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/README.md +49 -18
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +30 -7
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +108 -53
- package/lib/capabilities.mjs +9 -10
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +13 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +52 -3
- package/lib/dataflow.mjs +176 -125
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/evidence/cli.mjs +14 -3
- package/lib/evidence/record.mjs +1 -1
- package/lib/evidence/store.mjs +44 -31
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +13 -1
- package/lib/hlasm/asm/sections.mjs +3 -1
- package/lib/hlasm/instr.mjs +25 -0
- package/lib/hlasm/macro/authorization.mjs +24 -4
- package/lib/hlasm/macro/datasets.mjs +30 -17
- package/lib/hlasm/macro/io.mjs +68 -45
- package/lib/hlasm/macro/le.mjs +2 -2
- package/lib/hlasm/macro/operator.mjs +32 -21
- package/lib/hlasm/macro/program.mjs +126 -84
- package/lib/hlasm/macro/recovery.mjs +10 -5
- package/lib/hlasm/macro/storage.mjs +64 -14
- package/lib/hlasm/macro/structured.mjs +1 -1
- package/lib/hlasm/model.mjs +35 -10
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +10 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +22 -8
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/ironwork.mjs +17 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +14 -8
- package/lib/kernel/shared-pass.mjs +34 -9
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +7 -3
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +41 -3
- package/lib/scan.mjs +7 -1
- package/lib/sets/abend.mjs +16 -6
- package/lib/sets/cics.mjs +15 -35
- package/lib/sets/compile.mjs +41 -15
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +18 -1
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +72 -12
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +6 -6
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +5 -3
- package/lib/sets/semantics.mjs +3 -0
- package/lib/sets/web.mjs +36 -22
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +432 -1
- package/rules/compliance-dora.json +414 -1
- package/rules/compliance-ffiec.json +414 -1
- package/rules/compliance-nist80053.json +466 -1
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.site.schema.json +116 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Parsers for the IMS PSB macros: PCB (TYPE=TP, DB or GSAM), SENSEG, SENFLD and PSBGEN, after IMS 15.6
|
|
3
|
+
// System Utilities, "Program Specification Block (PSB) Generation utility", and its PSBGEN messages.
|
|
4
|
+
import { ImsSyntax, sublist, integer, onlyKeywords } from '../operands.mjs';
|
|
5
|
+
|
|
6
|
+
const DB_PROCOPT = new Set('AGIRDPONTELSH');
|
|
7
|
+
const SENSEG_PROCOPT = new Set('GIRDAEPK');
|
|
8
|
+
const GSAM_PROCOPT = ['G', 'GS', 'L', 'LS'];
|
|
9
|
+
// O, N and T are written only in these forms (the PCB syntax diagram; the O, N, T and H rules).
|
|
10
|
+
const READ_WITHOUT_INTEGRITY = ['GO', 'GOP', 'GON', 'GONP', 'GOT', 'GOTP'];
|
|
11
|
+
// PGEN110 adds PLI and PL1 to the statement page's list.
|
|
12
|
+
const LANGS = ['COBOL', 'PL/I', 'PLI', 'PL1', 'ASSEM', 'PASCAL', 'JAVA', ''];
|
|
13
|
+
const POSITIONS = { SINGLE: 'SINGLE', S: 'SINGLE', MULTIPLE: 'MULTIPLE', M: 'MULTIPLE' };
|
|
14
|
+
|
|
15
|
+
const upper = (v) => (v == null ? null : String(v).trim().toUpperCase());
|
|
16
|
+
const value = (ops, key) => ops.keywords.get(key) || null;
|
|
17
|
+
|
|
18
|
+
function oneOf(macro, ops, key, allowed) {
|
|
19
|
+
const v = upper(ops.keywords.get(key));
|
|
20
|
+
if (v == null) return null;
|
|
21
|
+
if (!allowed.includes(v)) throw new ImsSyntax(`${macro} ${key} must be ${allowed.join(' or ')}`);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// YES or NO as a boolean, and the reference's default when the operand is absent.
|
|
26
|
+
const yesNo = (macro, ops, key, absent) => { const v = oneOf(macro, ops, key, ['YES', 'NO']); return v == null ? absent : v === 'YES'; };
|
|
27
|
+
|
|
28
|
+
function number(macro, ops, key, min, max) {
|
|
29
|
+
const raw = ops.keywords.get(key);
|
|
30
|
+
if (raw == null) return null;
|
|
31
|
+
const n = integer(raw);
|
|
32
|
+
if (n === null || n < min || n > max) throw new ImsSyntax(`${macro} ${key} must be an integer from ${min} to ${max}`);
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function short(macro, what, v, most = 8) {
|
|
37
|
+
if (v != null && v.length > most) throw new ImsSyntax(`${macro} ${what} ${v} is longer than ${most} characters`);
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A PCB's own name, from its label or PCBNAME: at most 8 characters, and DFS is IMS's prefix.
|
|
42
|
+
function pcbName(macro, what, v) {
|
|
43
|
+
short(macro, what, v);
|
|
44
|
+
if (v && v.startsWith('DFS')) throw new ImsSyntax(`${macro} ${what} ${v} begins with DFS, which is reserved for IMS`);
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The letters as written; each must be a processing option the statement takes, at most four (the
|
|
49
|
+
// catalog holds PROCOPT in four bytes for a PCB and for a SENSEG).
|
|
50
|
+
function procopt(macro, ops, letters) {
|
|
51
|
+
const v = upper(ops.keywords.get('PROCOPT'));
|
|
52
|
+
if (v == null) return null;
|
|
53
|
+
if (!v) throw new ImsSyntax(`${macro} PROCOPT is empty`);
|
|
54
|
+
for (const c of v) if (!letters.has(c)) throw new ImsSyntax(`${macro} PROCOPT ${v}: ${c} is not a processing option it takes`);
|
|
55
|
+
if (v.length > 4) throw new ImsSyntax(`${macro} PROCOPT ${v} has more than 4 options`);
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const PCB_KEYWORDS = {
|
|
60
|
+
TP: ['TYPE', 'LTERM', 'NAME', 'ALTRESP', 'SAMETRM', 'MODIFY', 'EXPRESS', 'PCBNAME', 'EXTERNALNAME', 'LIST', 'REMARKS'],
|
|
61
|
+
DB: ['TYPE', 'DBDNAME', 'NAME', 'DBVER', 'PCBNAME', 'EXTERNALNAME', 'PROCOPT', 'SB', 'KEYLEN', 'PROCSEQ', 'PROCSEQD', 'PSELOPT', 'ACCESS', 'VIEW', 'LIST', 'REMARKS', 'POS'],
|
|
62
|
+
GSAM: ['TYPE', 'DBDNAME', 'NAME', 'PROCOPT', 'PCBNAME', 'EXTERNALNAME', 'LIST', 'REMARKS'],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function parsePCB(st, ops) {
|
|
66
|
+
const type = upper(ops.keywords.get('TYPE'));
|
|
67
|
+
if (!type) throw new ImsSyntax('PCB requires TYPE');
|
|
68
|
+
if (!PCB_KEYWORDS[type]) throw new ImsSyntax(`PCB TYPE ${type} is not TP, DB or GSAM`);
|
|
69
|
+
const macro = `PCB TYPE=${type}`;
|
|
70
|
+
onlyKeywords(ops, macro, PCB_KEYWORDS[type]);
|
|
71
|
+
|
|
72
|
+
const label = pcbName(macro, 'label', st.name || null);
|
|
73
|
+
const pcbname = pcbName(macro, 'PCBNAME', value(ops, 'PCBNAME'));
|
|
74
|
+
if (label && pcbname) {
|
|
75
|
+
if (type !== 'DB') throw new ImsSyntax(`${macro} takes a label or PCBNAME, not both`);
|
|
76
|
+
if (label !== pcbname) throw new ImsSyntax(`${macro} label ${label} and PCBNAME ${pcbname} must be the same name`);
|
|
77
|
+
}
|
|
78
|
+
const name = label || pcbname;
|
|
79
|
+
const list = yesNo(macro, ops, 'LIST', true);
|
|
80
|
+
if (!list && !name) throw new ImsSyntax(`${macro} LIST=NO needs the PCB named by a label or PCBNAME`);
|
|
81
|
+
|
|
82
|
+
const named = ['LTERM', 'DBDNAME', 'NAME'].filter((k) => ops.keywords.has(k));
|
|
83
|
+
if (named.length > 1) throw new ImsSyntax(`${macro} takes one of ${named.join(' and ')}`);
|
|
84
|
+
for (const k of named) short(macro, k, value(ops, k));
|
|
85
|
+
|
|
86
|
+
const externalname = value(ops, 'EXTERNALNAME');
|
|
87
|
+
short(macro, 'EXTERNALNAME', externalname, 128);
|
|
88
|
+
if (externalname && externalname.startsWith('DFS')) throw new ImsSyntax(`${macro} EXTERNALNAME ${externalname} begins with DFS`);
|
|
89
|
+
|
|
90
|
+
const keywords = {};
|
|
91
|
+
for (const [k, v] of ops.keywords) keywords[k] = v;
|
|
92
|
+
const common = { kind: 'PCB', type, name, label, pcbname, externalname, list };
|
|
93
|
+
|
|
94
|
+
if (type === 'TP') {
|
|
95
|
+
const modify = yesNo(macro, ops, 'MODIFY', false);
|
|
96
|
+
const lterm = value(ops, 'LTERM');
|
|
97
|
+
const transaction = value(ops, 'NAME');
|
|
98
|
+
if (modify && (lterm || transaction)) throw new ImsSyntax(`${macro} MODIFY=YES takes no LTERM or NAME`);
|
|
99
|
+
if (!lterm && !transaction && !modify) throw new ImsSyntax(`${macro} requires LTERM or NAME unless MODIFY=YES`);
|
|
100
|
+
const destination = lterm ? { kind: 'LTERM', name: lterm } : transaction ? { kind: 'TRANSACTION', name: transaction } : null;
|
|
101
|
+
return {
|
|
102
|
+
...common,
|
|
103
|
+
destination,
|
|
104
|
+
altresp: yesNo(macro, ops, 'ALTRESP', false),
|
|
105
|
+
sametrm: yesNo(macro, ops, 'SAMETRM', false),
|
|
106
|
+
modify,
|
|
107
|
+
express: yesNo(macro, ops, 'EXPRESS', false),
|
|
108
|
+
keywords
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const dbd = value(ops, 'DBDNAME') || value(ops, 'NAME');
|
|
113
|
+
if (!dbd) throw new ImsSyntax(`${macro} requires DBDNAME or NAME`);
|
|
114
|
+
|
|
115
|
+
if (type === 'GSAM') {
|
|
116
|
+
if (!ops.keywords.has('PROCOPT')) throw new ImsSyntax(`${macro} requires PROCOPT`);
|
|
117
|
+
return { ...common, dbd, procopt: oneOf(macro, ops, 'PROCOPT', GSAM_PROCOPT), keywords };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const procseqd = value(ops, 'PROCSEQD');
|
|
121
|
+
for (const k of ['PSELOPT', 'ACCESS']) if (ops.keywords.has(k) && !procseqd) throw new ImsSyntax(`${macro} ${k} is valid only with PROCSEQD`);
|
|
122
|
+
const posRaw = upper(ops.keywords.get('POS'));
|
|
123
|
+
if (posRaw != null && !POSITIONS[posRaw]) throw new ImsSyntax(`${macro} POS must be SINGLE, S, MULTIPLE or M`);
|
|
124
|
+
if (!ops.keywords.has('KEYLEN')) throw new ImsSyntax(`${macro} requires KEYLEN`);
|
|
125
|
+
const letters = procopt(macro, ops, DB_PROCOPT);
|
|
126
|
+
if (letters && /[ONT]/.test(letters) && !READ_WITHOUT_INTEGRITY.includes(letters)) throw new ImsSyntax(`${macro} PROCOPT ${letters}: O, N and T are written only as ${READ_WITHOUT_INTEGRITY.join(', ')}`);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...common,
|
|
130
|
+
dbd,
|
|
131
|
+
dbver: number(macro, ops, 'DBVER', 0, 2147483647),
|
|
132
|
+
procopt: letters,
|
|
133
|
+
keylen: number(macro, ops, 'KEYLEN', 0, 32767),
|
|
134
|
+
pos: posRaw == null ? null : POSITIONS[posRaw],
|
|
135
|
+
procseq: short(macro, 'PROCSEQ', value(ops, 'PROCSEQ')),
|
|
136
|
+
procseqd,
|
|
137
|
+
pselopt: oneOf(macro, ops, 'PSELOPT', ['MULT', 'SNGL']),
|
|
138
|
+
access: pcbAccess(macro, ops),
|
|
139
|
+
sb: oneOf(macro, ops, 'SB', ['NO', 'COND']),
|
|
140
|
+
view: oneOf(macro, ops, 'VIEW', ['MSDB', 'MSDBL']),
|
|
141
|
+
keywords
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ACCESS=DB or ACCESS=INDEX (PCB237), INDEX also written (INDEX,VSAM) or (INDEX,SHISAM) on the statement page.
|
|
146
|
+
function pcbAccess(macro, ops) {
|
|
147
|
+
const raw = ops.keywords.get('ACCESS');
|
|
148
|
+
if (raw == null) return null;
|
|
149
|
+
const items = sublist(raw).map((x) => x.toUpperCase());
|
|
150
|
+
const bare = !raw.trim().startsWith('(');
|
|
151
|
+
if (bare && items.length === 1 && (items[0] === 'DB' || items[0] === 'INDEX')) return { type: items[0], method: null };
|
|
152
|
+
if (!bare && items.length === 2 && items[0] === 'INDEX' && (items[1] === 'VSAM' || items[1] === 'SHISAM')) return { type: 'INDEX', method: items[1] };
|
|
153
|
+
throw new ImsSyntax(`${macro} ACCESS must be DB, INDEX, (INDEX,VSAM) or (INDEX,SHISAM)`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseSENSEG(st, ops) {
|
|
157
|
+
const name = short('SENSEG', 'NAME', value(ops, 'NAME'));
|
|
158
|
+
if (!name) throw new ImsSyntax('SENSEG requires NAME');
|
|
159
|
+
const parentRaw = ops.keywords.get('PARENT');
|
|
160
|
+
if (parentRaw != null && !parentRaw.trim()) throw new ImsSyntax('SENSEG PARENT must be 0 or a segment name');
|
|
161
|
+
const parent = parentRaw == null || parentRaw.trim() === '0' ? null : short('SENSEG', 'PARENT', parentRaw.trim());
|
|
162
|
+
|
|
163
|
+
let indices = null;
|
|
164
|
+
const indicesRaw = ops.keywords.get('INDICES');
|
|
165
|
+
if (indicesRaw != null) {
|
|
166
|
+
indices = sublist(indicesRaw);
|
|
167
|
+
if (indices.some((x) => !x)) throw new ImsSyntax('SENSEG INDICES must be a secondary index name or a list of them');
|
|
168
|
+
if (indices.length > 32) throw new ImsSyntax('SENSEG INDICES names more than 32 secondary indexes');
|
|
169
|
+
for (const x of indices) short('SENSEG', 'INDICES name', x);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const letters = procopt('SENSEG', ops, SENSEG_PROCOPT);
|
|
173
|
+
const ssptr = subsetPointers(ops.keywords.get('SSPTR'));
|
|
174
|
+
if (ssptr && !parent) throw new ImsSyntax('SENSEG SSPTR is not supported on a root segment');
|
|
175
|
+
if (ssptr && letters && !/[ARID]/.test(letters)) {
|
|
176
|
+
const u = ssptr.find((p) => p.sensitivity === 'U');
|
|
177
|
+
if (u) throw new ImsSyntax(`SENSEG SSPTR pointer ${u.pointer} is update-sensitive, which PROCOPT ${letters} does not allow`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const keywords = {};
|
|
181
|
+
for (const [k, v] of ops.keywords) keywords[k] = v;
|
|
182
|
+
return { kind: 'SENSEG', name, parent, procopt: letters, ssptr, indices, keywords };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// SSPTR=((n[,R|U]),...): up to eight subset pointers numbered 1 to 8, read-sensitive unless U is given.
|
|
186
|
+
function subsetPointers(raw) {
|
|
187
|
+
if (raw == null) return null;
|
|
188
|
+
const shape = 'SENSEG SSPTR must be ((n[,R|U]),...)';
|
|
189
|
+
if (!raw.trim().startsWith('(')) throw new ImsSyntax(shape);
|
|
190
|
+
const entries = sublist(raw);
|
|
191
|
+
if (entries.length > 8) throw new ImsSyntax('SENSEG SSPTR defines more than 8 subset pointers');
|
|
192
|
+
return entries.map((e) => {
|
|
193
|
+
if (!e.startsWith('(')) throw new ImsSyntax(shape);
|
|
194
|
+
const [n, sensitivity = 'R', ...rest] = sublist(e).map((x) => x.toUpperCase());
|
|
195
|
+
const pointer = integer(n);
|
|
196
|
+
if (rest.length || pointer === null || pointer < 1 || pointer > 8) throw new ImsSyntax('SENSEG SSPTR pointer number must be 1 to 8');
|
|
197
|
+
if (sensitivity !== 'R' && sensitivity !== 'U') throw new ImsSyntax('SENSEG SSPTR sensitivity must be R or U');
|
|
198
|
+
return { pointer, sensitivity };
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseSENFLD(st, ops) {
|
|
203
|
+
const name = short('SENFLD', 'NAME', value(ops, 'NAME'));
|
|
204
|
+
if (!name) throw new ImsSyntax('SENFLD requires NAME');
|
|
205
|
+
if (!ops.keywords.has('START')) throw new ImsSyntax('SENFLD requires START');
|
|
206
|
+
if (ops.keywords.has('REPLACE') && ops.keywords.has('REPL')) throw new ImsSyntax('SENFLD takes REPLACE or REPL, not both');
|
|
207
|
+
const key = ops.keywords.has('REPL') ? 'REPL' : 'REPLACE';
|
|
208
|
+
const replace = oneOf('SENFLD', ops, key, ['YES', 'Y', 'NO', 'N']);
|
|
209
|
+
|
|
210
|
+
const keywords = {};
|
|
211
|
+
for (const [k, v] of ops.keywords) keywords[k] = v;
|
|
212
|
+
return {
|
|
213
|
+
kind: 'SENFLD',
|
|
214
|
+
name,
|
|
215
|
+
start: number('SENFLD', ops, 'START', 1, 32767),
|
|
216
|
+
replace: replace == null || replace.startsWith('Y'),
|
|
217
|
+
keywords
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parsePSBGEN(st, ops) {
|
|
222
|
+
const psbname = short('PSBGEN', 'PSBNAME', value(ops, 'PSBNAME'));
|
|
223
|
+
if (!psbname) throw new ImsSyntax('PSBGEN requires PSBNAME');
|
|
224
|
+
const langRaw = upper(ops.keywords.get('LANG'));
|
|
225
|
+
if (langRaw != null && !LANGS.includes(langRaw)) throw new ImsSyntax('PSBGEN LANG must be COBOL, PL/I, PLI, PL1, ASSEM, PASCAL, JAVA or blank');
|
|
226
|
+
|
|
227
|
+
let ioeropn = null;
|
|
228
|
+
const ioeropnRaw = ops.keywords.get('IOEROPN');
|
|
229
|
+
if (ioeropnRaw != null) {
|
|
230
|
+
const parened = ioeropnRaw.trim().startsWith('(');
|
|
231
|
+
const items = sublist(ioeropnRaw).map((x) => x.toUpperCase());
|
|
232
|
+
const code = integer(items[0]);
|
|
233
|
+
const wtor = parened && items.length === 2 && items[1] === 'WTOR';
|
|
234
|
+
if (code === null || code > 4095 || (parened ? !wtor : items.length !== 1)) throw new ImsSyntax('PSBGEN IOEROPN must be n or (n,WTOR), n from 0 to 4095');
|
|
235
|
+
ioeropn = { code, wtor };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const keywords = {};
|
|
239
|
+
for (const [k, v] of ops.keywords) keywords[k] = v;
|
|
240
|
+
return {
|
|
241
|
+
kind: 'PSBGEN',
|
|
242
|
+
psbname,
|
|
243
|
+
dblevel: oneOf('PSBGEN', ops, 'DBLEVEL', ['CURR', 'BASE']),
|
|
244
|
+
lang: langRaw,
|
|
245
|
+
maxq: number('PSBGEN', ops, 'MAXQ', 0, 32767),
|
|
246
|
+
cmpat: yesNo('PSBGEN', ops, 'CMPAT', false),
|
|
247
|
+
ioasize: number('PSBGEN', ops, 'IOASIZE', 0, 256000),
|
|
248
|
+
ssasize: number('PSBGEN', ops, 'SSASIZE', 0, 256000),
|
|
249
|
+
ioeropn,
|
|
250
|
+
olic: yesNo('PSBGEN', ops, 'OLIC', false),
|
|
251
|
+
gsrolbok: yesNo('PSBGEN', ops, 'GSROLBOK', false),
|
|
252
|
+
lockmax: number('PSBGEN', ops, 'LOCKMAX', 0, 255),
|
|
253
|
+
keywords
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// REMARKS is 1 to 256 characters inside its quotes, with no double quote, <, > or &.
|
|
258
|
+
function remarks(macro, ops) {
|
|
259
|
+
const raw = ops.keywords.get('REMARKS');
|
|
260
|
+
if (raw == null) return;
|
|
261
|
+
const text = /^'([\s\S]*)'$/.exec(raw.trim())?.[1] ?? raw.trim();
|
|
262
|
+
if (!text || text.length > 256) throw new ImsSyntax(`${macro} REMARKS must be 1 to 256 characters`);
|
|
263
|
+
if (/["<>&]/.test(text)) throw new ImsSyntax(`${macro} REMARKS cannot contain a double quote, <, > or &`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const KEYWORDS = {
|
|
267
|
+
SENSEG: ['NAME', 'PARENT', 'PROCOPT', 'SSPTR', 'INDICES', 'REMARKS'],
|
|
268
|
+
SENFLD: ['NAME', 'START', 'REPLACE', 'REPL', 'REMARKS'],
|
|
269
|
+
PSBGEN: ['PSBNAME', 'DBLEVEL', 'LANG', 'MAXQ', 'CMPAT', 'IOASIZE', 'SSASIZE', 'IOEROPN', 'OLIC', 'GSROLBOK', 'LOCKMAX', 'REMARKS'],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// An operand field that ends in a comma was meant to go on, but the next card was not read as its
|
|
273
|
+
// continuation (no column-72 mark), so operands such as PROCOPT never reached the parser.
|
|
274
|
+
const read = (macro, parse) => (st, ops) => {
|
|
275
|
+
if (/,\s*$/.test(st.field)) throw new ImsSyntax(`${macro} operands end in a comma: the operands meant to follow were not read as a continuation`);
|
|
276
|
+
if (KEYWORDS[macro]) onlyKeywords(ops, macro, KEYWORDS[macro]);
|
|
277
|
+
remarks(macro, ops);
|
|
278
|
+
return parse(st, ops);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const parsers = Object.freeze({
|
|
282
|
+
PCB: read('PCB', parsePCB),
|
|
283
|
+
SENSEG: read('SENSEG', parseSENSEG),
|
|
284
|
+
SENFLD: read('SENFLD', parseSENFLD),
|
|
285
|
+
PSBGEN: read('PSBGEN', parsePSBGEN)
|
|
286
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// A DBD or PSB as its generation utility builds it: segments with their fields, or PCBs with their
|
|
3
|
+
// sensitive segments, and what DBDGEN or PSBGEN refuses in it, each under its message number in IMS
|
|
4
|
+
// 15.6 Messages and Codes, Volume 2. A statement the reader could not read may hide a segment, so the
|
|
5
|
+
// checks that need every segment named run only while none has been missed.
|
|
6
|
+
import { readIms, parseImsStatement, IMS_MACROS } from './read.mjs';
|
|
7
|
+
import { sublist } from './operands.mjs';
|
|
8
|
+
|
|
9
|
+
const DBD_MACROS = new Set(['DBD', 'DATASET', 'AREA', 'SEGM', 'FIELD', 'LCHILD', 'XDFLD', 'DBDGEN']);
|
|
10
|
+
const PSB_MACROS = new Set(['PCB', 'SENSEG', 'SENFLD', 'PSBGEN']);
|
|
11
|
+
|
|
12
|
+
export function modelOf(text) {
|
|
13
|
+
const read = readIms(text).statements.map((st) => ({ st, r: parseImsStatement(st) }));
|
|
14
|
+
const first = read.find(({ st }) => DBD_MACROS.has(st.operation) || PSB_MACROS.has(st.operation));
|
|
15
|
+
if (!first) return null;
|
|
16
|
+
return DBD_MACROS.has(first.st.operation) ? dbdModel(read) : psbModel(read);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// PARENT=name, PARENT=((name,SNGL|DBLE),(lparent,VIRTUAL|PHYSICAL,dbname)) or PARENT=0.
|
|
20
|
+
function parents(raw) {
|
|
21
|
+
if (raw == null) return { physical: null, logical: null };
|
|
22
|
+
const [first, second] = sublist(raw);
|
|
23
|
+
const physical = first.startsWith('(') ? sublist(first)[0] : first;
|
|
24
|
+
const logical = second ? sublist(second)[0] : null;
|
|
25
|
+
return { physical: physical === '0' ? null : physical, logical };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const unreadOf = (st, r) => ({ line: st.line, operation: st.operation ?? null, reason: r.reason ?? r.status });
|
|
29
|
+
|
|
30
|
+
// A statement that is not an IMS macro (a macro call, COPY) or a SEGM the reader refused may define a
|
|
31
|
+
// segment the model never saw.
|
|
32
|
+
const mayHideSegment = (st) => st.operation === 'SEGM' || !IMS_MACROS.has(st.operation);
|
|
33
|
+
|
|
34
|
+
function dbdModel(read) {
|
|
35
|
+
const model = { kind: 'DBD', name: null, access: null, segments: [], unread: [], problems: [], complete: true };
|
|
36
|
+
const problem = (line, message, text) => model.problems.push({ line, message, text });
|
|
37
|
+
const segments = new Map();
|
|
38
|
+
let segment; // the SEGM the next FIELD belongs to: undefined before any, null after an unread one
|
|
39
|
+
|
|
40
|
+
for (const { st, r } of read) {
|
|
41
|
+
if (r.status !== 'parsed') {
|
|
42
|
+
model.unread.push(unreadOf(st, r));
|
|
43
|
+
if (mayHideSegment(st)) model.complete = false;
|
|
44
|
+
if (st.operation === 'SEGM') segment = null;
|
|
45
|
+
else if (segment && (st.operation === 'FIELD' || st.operation === 'XDFLD')) segment.fieldStatements++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const n = r.node;
|
|
49
|
+
if (n.kind === 'DBD') {
|
|
50
|
+
model.name = n.name;
|
|
51
|
+
model.access = n.access;
|
|
52
|
+
} else if (n.kind === 'SEGM') {
|
|
53
|
+
const { physical, logical } = parents(n.keywords.PARENT);
|
|
54
|
+
segment = { name: n.name, parent: physical, logicalParent: logical, bytes: n.bytes, line: st.line, fields: [], fieldStatements: 0 };
|
|
55
|
+
if (segments.has(n.name)) problem(st.line, 'SEGM140', `segment ${n.name} is defined twice`);
|
|
56
|
+
else if (physical && !segments.has(physical) && model.complete) problem(st.line, 'SEGM170', `segment ${n.name} names parent ${physical}, which no earlier SEGM defines`);
|
|
57
|
+
segments.set(n.name, segment);
|
|
58
|
+
model.segments.push(segment);
|
|
59
|
+
} else if (n.kind === 'FIELD' || n.kind === 'XDFLD') {
|
|
60
|
+
if (segment === undefined) { problem(st.line, 'FLD100', `${n.kind} ${n.name ?? n.keywords.NAME} comes before any SEGM`); continue; }
|
|
61
|
+
if (segment === null) continue;
|
|
62
|
+
field(segment, n, st.line, problem);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const s of model.segments) {
|
|
66
|
+
const seq = s.fields.filter((f) => f.seq);
|
|
67
|
+
if (seq.length > 1 && !s.logicalParent) problem(seq[1].line, 'FLD120', `segment ${s.name} has ${seq.length} sequence fields`);
|
|
68
|
+
}
|
|
69
|
+
return model;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function field(segment, n, line, problem) {
|
|
73
|
+
const position = segment.fieldStatements++;
|
|
74
|
+
const name = n.kind === 'FIELD' ? n.name : n.keywords.NAME;
|
|
75
|
+
if (segment.fields.some((f) => f.name === name)) problem(line, 'FLD190', `field ${name} is defined twice in segment ${segment.name}`);
|
|
76
|
+
if (n.kind === 'XDFLD') { segment.fields.push({ name, kind: 'XDFLD', seq: false, line }); return; }
|
|
77
|
+
|
|
78
|
+
const f = { name, kind: 'FIELD', seq: n.seq, unique: n.unique, bytes: n.bytes, start: n.start, type: n.type, line };
|
|
79
|
+
if (f.seq && position > 0 && !segment.fields.some((x) => x.seq)) problem(line, 'FLD230', `sequence field ${name} is not the first field of segment ${segment.name}`);
|
|
80
|
+
// System-related fields (/SX, /CK) take START relative to the concatenated key, not the segment.
|
|
81
|
+
const systemRelated = name.startsWith('/');
|
|
82
|
+
if (!systemRelated && segment.bytes && f.start != null && f.bytes != null && f.start + f.bytes - 1 > segment.bytes.max) {
|
|
83
|
+
problem(line, 'FLD170', `field ${name} ends at byte ${f.start + f.bytes - 1}, past segment ${segment.name}'s ${segment.bytes.max} bytes`);
|
|
84
|
+
}
|
|
85
|
+
segment.fields.push(f);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function psbModel(read) {
|
|
89
|
+
const model = { kind: 'PSB', name: null, lang: null, pcbs: [], unread: [], problems: [] };
|
|
90
|
+
const problem = (line, message, text) => model.problems.push({ line, message, text });
|
|
91
|
+
let pcb; // the PCB the next SENSEG belongs to: undefined before any, null after an unread one
|
|
92
|
+
|
|
93
|
+
for (const { st, r } of read) {
|
|
94
|
+
if (r.status !== 'parsed') {
|
|
95
|
+
model.unread.push(unreadOf(st, r));
|
|
96
|
+
if (st.operation === 'PCB') pcb = null;
|
|
97
|
+
else if (pcb && (st.operation === 'SENSEG' || !IMS_MACROS.has(st.operation))) pcb.complete = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const n = r.node;
|
|
101
|
+
if (n.kind === 'PSBGEN') {
|
|
102
|
+
model.name = n.psbname;
|
|
103
|
+
model.lang = n.lang;
|
|
104
|
+
} else if (n.kind === 'PCB') {
|
|
105
|
+
pcb = {
|
|
106
|
+
name: n.name ?? `PCB ${model.pcbs.length + 1}`,
|
|
107
|
+
type: n.type,
|
|
108
|
+
dbd: n.dbd ?? null,
|
|
109
|
+
// PROCOPT=A is the default (Full-function or Fast Path database PCB statement).
|
|
110
|
+
procopt: n.type === 'DB' ? n.procopt ?? 'A' : n.procopt ?? null,
|
|
111
|
+
procoptWritten: n.procopt ?? null,
|
|
112
|
+
keylen: n.keylen ?? null,
|
|
113
|
+
procseq: n.procseq ?? n.procseqd ?? null,
|
|
114
|
+
line: st.line,
|
|
115
|
+
sensegs: [],
|
|
116
|
+
complete: true,
|
|
117
|
+
};
|
|
118
|
+
model.pcbs.push(pcb);
|
|
119
|
+
} else if (n.kind === 'SENSEG') {
|
|
120
|
+
if (pcb === undefined) { problem(st.line, 'SEG110', `SENSEG ${n.name} comes before any PCB`); continue; }
|
|
121
|
+
if (pcb === null) continue;
|
|
122
|
+
senseg(pcb, n, st.line, problem);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return model;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function senseg(pcb, n, line, problem) {
|
|
129
|
+
if (pcb.type !== 'DB') { problem(line, 'SEG140', `SENSEG ${n.name} follows ${pcb.type} PCB ${pcb.name}`); return; }
|
|
130
|
+
if (pcb.sensegs.some((s) => s.name === n.name)) problem(line, 'SEG150', `SENSEG ${n.name} appears twice in PCB ${pcb.name}`);
|
|
131
|
+
else if (n.parent && pcb.complete && !pcb.sensegs.some((s) => s.name === n.parent)) problem(line, 'SEG160', `SENSEG ${n.name} names parent ${n.parent}, which no earlier SENSEG of PCB ${pcb.name} defines`);
|
|
132
|
+
// A SENSEG without PROCOPT takes the PCB's (SENSEG statement).
|
|
133
|
+
pcb.sensegs.push({ name: n.name, parent: n.parent, procopt: n.procopt ?? pcb.procopt, procoptWritten: n.procopt ?? null, line });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The bytes of the concatenated key of a segment: the first sequence field of each segment from the
|
|
137
|
+
// root down, along the DBD's physical parents. A segment with no sequence field adds nothing here, so
|
|
138
|
+
// the figure is never more than IMS's. Null when the path leaves the segments the model holds.
|
|
139
|
+
export function concatenatedKey(dbd, name) {
|
|
140
|
+
const byName = new Map(dbd.segments.map((s) => [s.name, s]));
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
let total = 0;
|
|
143
|
+
for (let at = name; at; at = byName.get(at).parent) {
|
|
144
|
+
if (!byName.has(at) || seen.has(at)) return null;
|
|
145
|
+
seen.add(at);
|
|
146
|
+
total += byName.get(at).fields.find((f) => f.seq)?.bytes ?? 0;
|
|
147
|
+
}
|
|
148
|
+
return total;
|
|
149
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// What an IMS macro parser needs to read operands: the refusal it throws, and sublists and integers.
|
|
3
|
+
import { splitOperands } from '../cards.mjs';
|
|
4
|
+
|
|
5
|
+
export class ImsSyntax extends Error {
|
|
6
|
+
constructor(message) { super(message); this.name = 'ImsSyntax'; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// (A,B) and A both as a list of trimmed entries; null when absent.
|
|
10
|
+
export const sublist = (v) => {
|
|
11
|
+
if (v == null) return null;
|
|
12
|
+
const inner = /^\(([\s\S]*)\)$/.exec(String(v).trim());
|
|
13
|
+
return (inner ? splitOperands(inner[1]) : [String(v)]).map((x) => x.trim());
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const integer = (v) => (/^\d+$/.test(String(v ?? '').trim()) ? Number(String(v).trim()) : null);
|
|
17
|
+
|
|
18
|
+
// IMS macro operands are keywords: a positional operand, or a keyword the macro does not take, is
|
|
19
|
+
// refused so that a statement counts as read only when every operand on it was understood.
|
|
20
|
+
export function onlyKeywords(ops, macro, allowed) {
|
|
21
|
+
if (ops.positional.length) throw new ImsSyntax(`${macro} takes no positional operand: ${ops.positional[0]}`);
|
|
22
|
+
for (const k of ops.keywords.keys()) if (!allowed.includes(k)) throw new ImsSyntax(`${macro} has no ${k} operand`);
|
|
23
|
+
}
|
package/lib/ims/read.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// IMS DBD and PSB source, read as the assembler reads its macro cards (lib/bms.mjs folds them), and
|
|
3
|
+
// each statement parsed by its macro's parser: DBD, DATASET, SEGM, FIELD, LCHILD, XDFLD and DBDGEN
|
|
4
|
+
// for a database, PCB, SENSEG, SENFLD and PSBGEN for a program's view of it.
|
|
5
|
+
import { foldStatements } from '../bms.mjs';
|
|
6
|
+
import { parseOperands } from '../cards.mjs';
|
|
7
|
+
import { ImsSyntax } from './operands.mjs';
|
|
8
|
+
import { parsers as dbd } from './macro/dbd.mjs';
|
|
9
|
+
import { parsers as psb } from './macro/psb.mjs';
|
|
10
|
+
|
|
11
|
+
export const PARSERS = Object.freeze({ ...dbd, ...psb });
|
|
12
|
+
|
|
13
|
+
// Assembler instructions around the macros, which carry nothing for IMS.
|
|
14
|
+
const ASSEMBLER = new Set(['TITLE', 'PRINT', 'EJECT', 'SPACE', 'END', 'FINISH', 'ICTL', 'ISEQ', 'PUNCH']);
|
|
15
|
+
export const IMS_MACROS = new Set(['DBD', 'DATASET', 'AREA', 'SEGM', 'FIELD', 'LCHILD', 'XDFLD', 'DBDGEN', 'PCB', 'SENSEG', 'SENFLD', 'PSBGEN']);
|
|
16
|
+
|
|
17
|
+
export function readIms(text) {
|
|
18
|
+
const { statements, diags } = foldStatements(text);
|
|
19
|
+
return { statements: statements.filter((s) => s.kind !== 'comment'), diags };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// { kind, status, node?, reason? } as the PL/I reader gives them: parsed, unbuilt (no parser for the
|
|
23
|
+
// macro yet), unparsed (the parser refused an operand) or unknown (not an IMS macro).
|
|
24
|
+
export function parseImsStatement(st) {
|
|
25
|
+
if (st.kind !== 'statement') return { kind: 'UNREADABLE', status: 'unknown', reason: 'no operation field' };
|
|
26
|
+
const kind = st.operation;
|
|
27
|
+
if (ASSEMBLER.has(kind)) return { kind: 'ASSEMBLER', status: 'parsed', node: { kind, operation: kind } };
|
|
28
|
+
if (!IMS_MACROS.has(kind)) return { kind: 'UNKNOWN', status: 'unknown', reason: `${kind} is not an IMS DBD or PSB macro` };
|
|
29
|
+
const parser = PARSERS[kind];
|
|
30
|
+
if (!parser) return { kind, status: 'unbuilt' };
|
|
31
|
+
try {
|
|
32
|
+
return { kind, status: 'parsed', node: parser(st, parseOperands(st.field)) };
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e instanceof ImsSyntax) return { kind, status: 'unparsed', reason: e.message };
|
|
35
|
+
return { kind, status: 'unparsed', reason: `parser error: ${e && e.message}`, crash: true };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Rules over IMS DBD and PSB source alone: what a database PCB lets a program change, SENSEGs naming
|
|
3
|
+
// segments their DBD does not define, and what DBDGEN, PSBGEN or ACBGEN refuses.
|
|
4
|
+
import { modelOf, concatenatedKey } from './model.mjs';
|
|
5
|
+
import { DLI_FUNCTIONS } from './dli.mjs';
|
|
6
|
+
|
|
7
|
+
export const IMS_RULES = Object.freeze({
|
|
8
|
+
'ims-pcb-procopt-all': { sev: 'info', evidence: 'context', cwe: 'CWE-250', text: 'A database PCB lets the program get, insert, replace and delete its sensitive segments' },
|
|
9
|
+
'ims-senseg-unknown-segment': {
|
|
10
|
+
sev: 'low', evidence: 'construct', cwe: 'CWE-1076',
|
|
11
|
+
text: "A SENSEG names a segment its PCB's DBD does not define, which ends the program with abend U0914",
|
|
12
|
+
impact: 'The PSB cannot be scheduled against the DBD in this tree: the program abends before it runs, or the DBD here is not the one production uses',
|
|
13
|
+
remedy: 'Correct the SENSEG name, or bring the DBD in the tree up to the one the PSB was generated against',
|
|
14
|
+
},
|
|
15
|
+
'ims-procopt-broader-than-used': {
|
|
16
|
+
sev: 'high', evidence: 'construct', cwe: 'CWE-250',
|
|
17
|
+
text: "A PSB grants a processing option that no DL/I call of the program it is named for uses",
|
|
18
|
+
impact: 'The program can insert, replace or delete segments it never needs to change, so a flaw in it, or input that steers it, can alter the database beyond what its own calls show',
|
|
19
|
+
remedy: "Set the PCB's PROCOPT, or each SENSEG's, to the options the program's calls use, and regenerate the PSB and its ACB",
|
|
20
|
+
},
|
|
21
|
+
'ims-definition-inconsistent': {
|
|
22
|
+
sev: 'low', evidence: 'construct', cwe: 'CWE-1076',
|
|
23
|
+
text: 'A DBD or PSB holds what DBDGEN, PSBGEN or ACBGEN refuses',
|
|
24
|
+
impact: 'The generation step refuses the definition, or the program abends when it is scheduled (U0919 for a KEYLEN too short), so the source in the tree does not describe what runs',
|
|
25
|
+
remedy: 'Correct the statement the message names and regenerate; where production runs a different definition, keep that one in the tree',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const OPTION_NAME = { G: 'get', I: 'insert', R: 'replace', D: 'delete' };
|
|
30
|
+
|
|
31
|
+
// The database options a PROCOPT grants: A is all four, and R and D each imply G.
|
|
32
|
+
function granted(procopt) {
|
|
33
|
+
const p = String(procopt || '');
|
|
34
|
+
const out = new Set(p.includes('A') ? ['G', 'I', 'R', 'D'] : [...p].filter((c) => OPTION_NAME[c]));
|
|
35
|
+
if (out.has('R') || out.has('D')) out.add('G');
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Each PSB a program is known to run with, against the DL/I calls of every such program. A program
|
|
40
|
+
// is linked to a PSB by a DFSRRC00 step's PARM or by its own scheduling call, [{ program, psb, at }],
|
|
41
|
+
// and a PSB nothing links to is taken as its program's when PSBGEN PSBNAME, or else its file name,
|
|
42
|
+
// names one. `programs` holds [{ id, path, line, functions }], functions null when a program's calls
|
|
43
|
+
// cannot all be told. A call is not tied to its PCB, so only an option no call of any of the PSB's
|
|
44
|
+
// programs uses is reported, on each database PCB that grants it.
|
|
45
|
+
export function checkProcoptAgainstCalls(models, programs, links = []) {
|
|
46
|
+
const byId = new Map();
|
|
47
|
+
for (const p of programs) byId.set(p.id.toUpperCase(), byId.has(p.id.toUpperCase()) ? null : p);
|
|
48
|
+
const users = new Map();
|
|
49
|
+
for (const l of links) {
|
|
50
|
+
if (!users.has(l.psb)) users.set(l.psb, new Map());
|
|
51
|
+
users.get(l.psb).set(l.program, l.at);
|
|
52
|
+
}
|
|
53
|
+
const findings = [];
|
|
54
|
+
for (const { path, model } of models) {
|
|
55
|
+
if (model.kind !== 'PSB') continue;
|
|
56
|
+
const fileName = path.split('/').pop().replace(/\.[^.]*$/, '').toUpperCase();
|
|
57
|
+
const name = String(model.name || fileName).toUpperCase();
|
|
58
|
+
let linked = users.get(name);
|
|
59
|
+
if (!linked) {
|
|
60
|
+
const own = byId.has(name) ? name : byId.has(fileName) ? fileName : null;
|
|
61
|
+
if (!own) continue;
|
|
62
|
+
linked = new Map([[own, null]]);
|
|
63
|
+
}
|
|
64
|
+
const progs = [...linked.keys()].map((id) => byId.get(id));
|
|
65
|
+
if (progs.some((p) => !p || !p.functions)) continue;
|
|
66
|
+
const functions = new Set(progs.flatMap((p) => [...p.functions]));
|
|
67
|
+
const used = new Set([...functions].map((fn) => DLI_FUNCTIONS[fn]));
|
|
68
|
+
if (used.has('R') || used.has('D')) used.add('G');
|
|
69
|
+
const who = progs.map((p) => `${p.id} (${p.path}:${p.line}${linked.get(p.id.toUpperCase()) ? `, run with it at ${linked.get(p.id.toUpperCase())}` : ''})`).join(', ');
|
|
70
|
+
for (const pcb of model.pcbs.filter((x) => x.type === 'DB')) {
|
|
71
|
+
const grants = new Set([...granted(pcb.procopt), ...pcb.sensegs.flatMap((s) => [...granted(s.procopt)])]);
|
|
72
|
+
const unused = ['G', 'I', 'R', 'D'].filter((c) => grants.has(c) && !used.has(c));
|
|
73
|
+
if (!unused.length) continue;
|
|
74
|
+
const written = pcb.procoptWritten ? `PROCOPT=${pcb.procoptWritten}` : 'PROCOPT omitted, so A,';
|
|
75
|
+
findings.push({ rule: 'ims-procopt-broader-than-used', path, line: pcb.line, sev: IMS_RULES['ims-procopt-broader-than-used'].sev,
|
|
76
|
+
detail: `PSB ${model.name ?? fileName} PCB ${pcb.name} on DBD ${pcb.dbd}: ${written} grants ${unused.map((c) => OPTION_NAME[c]).join(', ')}, which no DL/I call of ${who} uses; ${progs.length > 1 ? 'they issue' : 'it issues'} ${[...functions].sort().join(', ')}` });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A holds G, I, R and D; R and D each imply G, so I, R and D together are all four.
|
|
83
|
+
const changesEverything = (procopt) => !!procopt && (procopt.includes('A') || ['I', 'R', 'D'].every((c) => procopt.includes(c)));
|
|
84
|
+
|
|
85
|
+
export const checkIms = (files) => checkModels(files.map(({ path, text }) => ({ path, model: modelOf(text) })).filter((m) => m.model));
|
|
86
|
+
|
|
87
|
+
// Findings over models already read, [{ path, model }], DBDs and PSBs together so a PCB can be
|
|
88
|
+
// checked against its DBD.
|
|
89
|
+
export function checkModels(models) {
|
|
90
|
+
const dbds = new Map();
|
|
91
|
+
for (const { model } of models.filter((m) => m.model.kind === 'DBD' && m.model.name)) {
|
|
92
|
+
dbds.set(model.name, dbds.has(model.name) ? null : model);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const findings = [];
|
|
96
|
+
for (const { path, model } of models) {
|
|
97
|
+
const at = (rule, line, detail) => findings.push({ rule, path, line, sev: IMS_RULES[rule].sev, detail });
|
|
98
|
+
for (const p of model.problems) at('ims-definition-inconsistent', p.line, `${p.message}: ${p.text}`);
|
|
99
|
+
if (model.kind !== 'PSB') continue;
|
|
100
|
+
for (const pcb of model.pcbs.filter((x) => x.type === 'DB')) {
|
|
101
|
+
const where = `PSB ${model.name ?? '(no PSBGEN)'} PCB ${pcb.name} on DBD ${pcb.dbd}`;
|
|
102
|
+
procoptAll(pcb, where, at);
|
|
103
|
+
const dbd = dbds.get(pcb.dbd);
|
|
104
|
+
if (dbd) againstDbd(pcb, dbd, where, at);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function procoptAll(pcb, where, at) {
|
|
111
|
+
const full = pcb.sensegs.filter((s) => changesEverything(s.procopt));
|
|
112
|
+
if (pcb.sensegs.length ? !full.length : !changesEverything(pcb.procopt)) return;
|
|
113
|
+
const written = pcb.procoptWritten ? `PROCOPT=${pcb.procoptWritten}` : 'PROCOPT omitted, so A,';
|
|
114
|
+
const segments = pcb.sensegs.length ? `segments ${full.map((s) => s.name).join(', ')}` : 'every sensitive segment';
|
|
115
|
+
at('ims-pcb-procopt-all', pcb.line, `${where}: ${written} gives get, insert, replace and delete on ${segments}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Checks that need the PCB's DBD: SENSEGs it does not define (abend U0914), and a KEYLEN shorter than
|
|
119
|
+
// the longest concatenated key (DFS0919I at ACBGEN). A secondary processing sequence or a logical DBD
|
|
120
|
+
// builds keys from other segments, so KEYLEN is checked only on the physical path of a physical DBD.
|
|
121
|
+
function againstDbd(pcb, dbd, where, at) {
|
|
122
|
+
const defined = new Set(dbd.segments.map((s) => s.name));
|
|
123
|
+
if (dbd.complete) {
|
|
124
|
+
for (const s of pcb.sensegs.filter((x) => !defined.has(x.name))) at('ims-senseg-unknown-segment', s.line, `${where}: SENSEG ${s.name} is not a segment of DBD ${dbd.name}`);
|
|
125
|
+
}
|
|
126
|
+
if (pcb.keylen == null || pcb.procseq || dbd.access?.type === 'LOGICAL') return;
|
|
127
|
+
let longest = null;
|
|
128
|
+
for (const s of pcb.sensegs) {
|
|
129
|
+
const key = concatenatedKey(dbd, s.name);
|
|
130
|
+
if (key != null && (!longest || key > longest.key)) longest = { key, name: s.name };
|
|
131
|
+
}
|
|
132
|
+
if (longest && longest.key > pcb.keylen) {
|
|
133
|
+
at('ims-definition-inconsistent', pcb.line, `DFS0919I: ${where} has KEYLEN=${pcb.keylen}, shorter than the ${longest.key}-byte concatenated key of ${longest.name}`);
|
|
134
|
+
}
|
|
135
|
+
}
|