@portll/cobolwork 0.2.76 → 0.2.140
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 +66 -14
- package/bin/cobolwork.mjs +126 -9
- package/lib/arith.mjs +235 -0
- package/lib/baseline.mjs +5 -0
- package/lib/build.mjs +112 -30
- package/lib/capabilities.mjs +12 -7
- package/lib/card.mjs +18 -0
- package/lib/cics-commands.mjs +136 -125
- package/lib/consequence.mjs +29 -0
- package/lib/control.mjs +1169 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +276 -126
- package/lib/embedded-sql.mjs +110 -0
- package/lib/enterprise-options.mjs +255 -0
- package/lib/equivalence.mjs +124 -0
- package/lib/evidence/cli.mjs +65 -0
- package/lib/evidence/journal.mjs +94 -0
- package/lib/evidence/record.mjs +110 -0
- package/lib/evidence/run.mjs +111 -0
- package/lib/evidence/seal.mjs +177 -0
- package/lib/evidence/slsa.mjs +64 -0
- package/lib/evidence/sshsig.mjs +172 -0
- package/lib/evidence/store.mjs +170 -0
- package/lib/evidence/verify.mjs +167 -0
- package/lib/explain.mjs +9 -5
- package/lib/exploitability.mjs +263 -0
- package/lib/ftp.mjs +136 -0
- package/lib/ironwork.mjs +109 -0
- package/lib/jcl.mjs +27 -7
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/layout.mjs +202 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +80 -209
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +5 -84
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +235 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +119 -2
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +70 -106
- package/lib/sets/recon.mjs +2 -1
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +246 -0
- package/lib/sources.mjs +10 -0
- package/lib/tui/app.mjs +31 -13
- package/lib/tui/model.mjs +17 -2
- package/lib/utilities.mjs +229 -15
- package/lib/verify.mjs +128 -0
- package/package.json +1 -1
- package/rules/compliance-dora.json +507 -0
- package/rules/compliance-ffiec.json +507 -0
- package/rules/compliance-nist80053.json +507 -0
- package/schema/cobolwork.policy.schema.json +94 -16
package/lib/ftp.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { statementCard } from './card.mjs';
|
|
3
|
+
|
|
4
|
+
// IBM's batch FTP client takes the host and its options on PARM, or the host as the first line of
|
|
5
|
+
// its input, reads subcommands from INPUT, and logs on from NETRC or from its input. CardDemo's job
|
|
6
|
+
// gives the same lines on SYSIN, which is read here when there is no INPUT, though the page below
|
|
7
|
+
// does not say the client falls back to it.
|
|
8
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.halu001/ftpreq.htm
|
|
9
|
+
// A session is in cleartext unless something asks for TLS: -a TLS or -r TLS on PARM, or
|
|
10
|
+
// SECURE_MECHANISM TLS in an in-stream SYSFTPD. A SYSFTPD naming a data set is configuration this
|
|
11
|
+
// reader cannot see, so that step is not called cleartext either way.
|
|
12
|
+
// https://www.ibm.com/docs/en/zos/2.1.0?topic=commands-ftp-command
|
|
13
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.halz002/ftp_cust_client_for_tls.htm
|
|
14
|
+
// PUT, MPUT and APPEND send local files, and IBM's capitals - PUt, MPut, APpend, LCd - are the
|
|
15
|
+
// shortest spellings. A local name in quotes is a whole data set name, //DD: names a DD of the
|
|
16
|
+
// step, and anything else hangs off the local working directory, which only a quoted LCD fixes.
|
|
17
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/ftpsubcmd.htm
|
|
18
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/put.htm
|
|
19
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/mput.htm
|
|
20
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/append.htm
|
|
21
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/lcd.htm
|
|
22
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/dd_name_support.htm
|
|
23
|
+
// Get and MGet copy remote files in, each to the local name given or, with none, to the foreign
|
|
24
|
+
// file's own name, placed as a sent name is. SIte FILETYPE=JES makes the remote host submit what
|
|
25
|
+
// is sent as a job and return job output to a Get; FILETYPE=SQL makes it run a query. Either holds
|
|
26
|
+
// for the transfers after it, until another FILETYPE.
|
|
27
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/get.htm
|
|
28
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/mget.htm
|
|
29
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/site.htm
|
|
30
|
+
const FTP_SENDS = [[/^PUT?$/i, 'PUT'], [/^MP(?:UT?)?$/i, 'MPUT'], [/^AP(?:P(?:E(?:ND?)?)?)?$/i, 'APPEND']];
|
|
31
|
+
|
|
32
|
+
// Every subcommand on the page above, in IBM's spelling.
|
|
33
|
+
const FTP_SUBCOMMANDS = ['?', '!', 'ACCount', 'APpend', 'AUth', 'AScii', 'BIG5', 'BINary', 'BLock', 'CCc', 'CD',
|
|
34
|
+
'CDUp', 'CLEar', 'CLose', 'COMpress', 'CProtect', 'CWd', 'DEBug', 'DELEte', 'DELImit', 'DIr', 'DUMP', 'EBcdic',
|
|
35
|
+
'EUckanji', 'FEature', 'FIle', 'Get', 'GLob', 'HAngeul', 'HElp', 'Ibmkanji', 'JIS78kj', 'JIS83kj', 'Ksc5601',
|
|
36
|
+
'LANGuage', 'LCd', 'LOCSIte', 'LOCSTat', 'LMkdir', 'LPwd', 'LS', 'MDelete', 'MGet', 'MKdir', 'MKFifo', 'MOde',
|
|
37
|
+
'MPut', 'MVSGet', 'MVSPut', 'NOop', 'Open', 'PAss', 'PRIvate', 'PROMpt', 'PROTect', 'PROXy', 'PUt', 'PWd', 'QUIt',
|
|
38
|
+
'QUOte', 'RECord', 'REName', 'REStart', 'RMdir', 'SAfe', 'SChinese', 'SENDPort', 'SENDSite', 'SIte', 'SJiskanji',
|
|
39
|
+
'SRestart', 'STAtus', 'STREam', 'STRucture', 'SUnique', 'SYstem', 'TChinese', 'TSO', 'TYpe', 'UCs2', 'User', 'Verbose']
|
|
40
|
+
.map((name) => ({ full: name.toUpperCase(), min: name.match(/^[^a-z]*/)[0].length }));
|
|
41
|
+
const subcommandOf = (word) => {
|
|
42
|
+
const w = word.toUpperCase();
|
|
43
|
+
const hit = FTP_SUBCOMMANDS.find((c) => w.length >= c.min && c.full.startsWith(w));
|
|
44
|
+
return hit ? hit.full : null;
|
|
45
|
+
};
|
|
46
|
+
// A value that stands for a password rather than being one: a symbolic, a mask, or the word itself.
|
|
47
|
+
const PLACEHOLDER = /^(&.*|\*+|X+|\?+|<.*>|\{.*\}|PASSWORD|PASSWD|PWD|PASS|SECRET)$/i;
|
|
48
|
+
|
|
49
|
+
// The session one FTP step runs, from its DDs grouped by name (ddGroups in lib/utilities.mjs).
|
|
50
|
+
export function ftpSession(step, dds) {
|
|
51
|
+
let host = null, tls = null;
|
|
52
|
+
const words = (step.parm || '').split('(')[0].trim().split(/\s+/).filter(Boolean);
|
|
53
|
+
for (let i = 0; i < words.length; i++) {
|
|
54
|
+
if (/^-[ar]$/i.test(words[i])) {
|
|
55
|
+
const mechanism = (words[++i] || '').toUpperCase();
|
|
56
|
+
if (mechanism === 'TLS') tls = 'asked';
|
|
57
|
+
else if (mechanism === 'NEVER' && !tls) tls = 'never';
|
|
58
|
+
} else if (!words[i].startsWith('-') && !host) host = words[i];
|
|
59
|
+
}
|
|
60
|
+
let configUnread = null;
|
|
61
|
+
for (const dd of dds.get('SYSFTPD') || []) {
|
|
62
|
+
if (dd.inStream) { if (tls !== 'never' && dd.inStream.some((l) => /^\s*SECURE_MECHANISM\s+TLS\b/i.test(l.text))) tls = 'asked'; }
|
|
63
|
+
else if (dd.dsn && !configUnread) configUnread = dd.dsn;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const input = dds.get('INPUT') || dds.get('SYSIN') || [];
|
|
67
|
+
const hostLine = !host && input[0]?.inStream;
|
|
68
|
+
let inputUnread = null, lcd = null, unixDir = false;
|
|
69
|
+
const sends = [], gets = [];
|
|
70
|
+
let filetype = null;
|
|
71
|
+
// With no NETRC to answer for it, the client asks for a user ID and a password and in batch reads
|
|
72
|
+
// them from its input after the host: "user password" on one line, or each on its own. A first
|
|
73
|
+
// line that is a subcommand means something else answered, a NETRC the client found for itself.
|
|
74
|
+
// USER and PASS give the same answers later in a session.
|
|
75
|
+
let expect = dds.has('NETRC') ? null : 'logon', user = null;
|
|
76
|
+
const passwords = [];
|
|
77
|
+
const password = (dd, line, value) => { if (!PLACEHOLDER.test(value)) passwords.push({ dd, line, user }); };
|
|
78
|
+
const local = (name) => {
|
|
79
|
+
const dd = /^\/\/DD:([A-Z$#@][A-Z0-9$#@]{0,7})$/i.exec(name);
|
|
80
|
+
if (dd) return { dd: dd[1].toUpperCase(), dsns: (dds.get(dd[1].toUpperCase()) || []).map((d) => d.dsn).filter(Boolean) };
|
|
81
|
+
const quoted = /^'(.+)'$/.exec(name);
|
|
82
|
+
if (quoted) return { dsns: [quoted[1].toUpperCase()] };
|
|
83
|
+
if (name.startsWith('/') || unixDir) return { dsns: [] };
|
|
84
|
+
return lcd ? { dsns: [`${lcd}.${name.toUpperCase()}`] } : { dsns: [], relative: name };
|
|
85
|
+
};
|
|
86
|
+
for (const dd of input) {
|
|
87
|
+
if (!dd.inStream) { if (dd.dsn && !inputUnread) inputUnread = dd.dsn; continue; }
|
|
88
|
+
for (const { line, text } of dd.inStream) {
|
|
89
|
+
const [verb, ...args] = statementCard(text).text.trim().split(/\s+/);
|
|
90
|
+
if (!verb) continue;
|
|
91
|
+
if (hostLine && !host) { host = verb; continue; }
|
|
92
|
+
const sub = subcommandOf(verb);
|
|
93
|
+
if (expect && !sub) {
|
|
94
|
+
if (expect === 'logon') user = verb;
|
|
95
|
+
if (expect === 'password') password(dd, line, verb);
|
|
96
|
+
else if (args[0]) password(dd, line, args[0]);
|
|
97
|
+
expect = expect === 'logon' && !args[0] ? 'password' : null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
expect = null;
|
|
101
|
+
if (sub === 'USER' && args[0]) {
|
|
102
|
+
user = args[0];
|
|
103
|
+
if (args[1]) password(dd, line, args[1]); else expect = 'password';
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (sub === 'PASS' && args[0]) { password(dd, line, args[0]); continue; }
|
|
107
|
+
if (/^LCD?$/i.test(verb) && args[0]) {
|
|
108
|
+
const quoted = /^'(.+)'$/.exec(args[0]);
|
|
109
|
+
unixDir = args[0].startsWith('/');
|
|
110
|
+
lcd = quoted ? quoted[1].toUpperCase() : lcd && !unixDir ? `${lcd}.${args[0].toUpperCase()}` : null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const site = sub === 'SITE' ? args : sub === 'QUOTE' && subcommandOf(args[0] || '') === 'SITE' ? args.slice(1) : null;
|
|
114
|
+
if (site) {
|
|
115
|
+
for (const a of site) { const t = /^FILE(?:T(?:Y(?:P(?:E)?)?)?)?=(\w+)$/i.exec(a); if (t) filetype = t[1].toUpperCase() === 'SEQ' ? null : t[1].toUpperCase(); }
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const typed = filetype ? { filetype } : {};
|
|
119
|
+
if (sub === 'GET' && args[0]) {
|
|
120
|
+
const to = args[1] && !args[1].startsWith('(') ? args[1] : args[0];
|
|
121
|
+
gets.push({ line, verb: 'GET', name: args[0], ...typed, ...local(to) });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (sub === 'MGET') {
|
|
125
|
+
for (const name of args.filter((a) => !a.startsWith('(') && !/[*?]/.test(a))) gets.push({ line, verb: 'MGET', name, ...typed, ...local(name) });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const sent = FTP_SENDS.find(([re]) => re.test(verb));
|
|
129
|
+
if (!sent) continue;
|
|
130
|
+
const foreign = sent[1] !== 'MPUT' && args[1] && !args[1].startsWith('(') ? args[1] : null;
|
|
131
|
+
for (const name of sent[1] === 'MPUT' ? args : args.slice(0, 1)) sends.push({ line, verb: sent[1], name, foreign: foreign || name, ...typed, ...local(name) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const cleartext = tls === 'never' || (!tls && !configUnread);
|
|
135
|
+
return { host, cleartext, configUnread, inputUnread, logon: dds.has('NETRC') ? 'NETRC' : null, sends, gets, passwords, input };
|
|
136
|
+
}
|
package/lib/ironwork.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// `ironwork check` over every program in a tree: IBM Enterprise COBOL's front end as ironwork models
|
|
3
|
+
// it, for an estate whose compile step runs on z/OS and so never meets the build gate otherwise.
|
|
4
|
+
// ironwork is run as a separate program, as cobc is; nothing here links it.
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { directoryTree } from './kernel/source-tree.mjs';
|
|
9
|
+
import { printable } from './kernel/printable.mjs';
|
|
10
|
+
import { buildFileIndex } from './parser.mjs';
|
|
11
|
+
import { isProgram, relPath } from './sources.mjs';
|
|
12
|
+
import { EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS } from './words.mjs';
|
|
13
|
+
|
|
14
|
+
// ironwork check exits with IBM's highest return code: 0 clean, 4 warnings only (compiled), 8 E,
|
|
15
|
+
// 12 S, 16 U. Only 0 and 4 produce a program.
|
|
16
|
+
const COMPILED = new Set([0, 4]);
|
|
17
|
+
const REJECTED = new Set([8, 12, 16]);
|
|
18
|
+
// Warnings and informational lines follow the errors, and say nothing about why a program failed.
|
|
19
|
+
const NOT_AN_ERROR = /^(?:informational:|.*?: warning: )/;
|
|
20
|
+
const PER_PROGRAM_MS = 60000;
|
|
21
|
+
const MAX_LISTED = 10;
|
|
22
|
+
|
|
23
|
+
// ironwork refuses by name what it does not model yet; that says nothing about the program. A name
|
|
24
|
+
// the CICS, DL/I or SQL translator declares it reports as undefined, without saying so, and that is
|
|
25
|
+
// read as its gap, not the program's.
|
|
26
|
+
const NOT_SUPPORTED = /\bnot supported\b/i;
|
|
27
|
+
const TRANSLATOR_NAMES = [EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS];
|
|
28
|
+
function notModelled(message) {
|
|
29
|
+
if (NOT_SUPPORTED.test(message)) return true;
|
|
30
|
+
const name = /^([A-Z][A-Z0-9-]*) is not defined$/.exec(message);
|
|
31
|
+
return !!name && TRANSLATOR_NAMES.some((s) => s.has(name[1]));
|
|
32
|
+
}
|
|
33
|
+
// A copy library the tree does not hold leaves the program unread, as it does for the scan.
|
|
34
|
+
const UNRESOLVED = /\bno such member in the copy libraries\b/i;
|
|
35
|
+
// A diagnostic can quote a literal from the program, and no output of the gate carries source text.
|
|
36
|
+
const redact = (s) => String(s).replace(/'[^']*'|"[^"]*"/g, "'…'");
|
|
37
|
+
|
|
38
|
+
// `file:line:col: message` or `file: message`, as ironwork's Error::place writes them. `file` is the
|
|
39
|
+
// program as it was passed, or the COPY member the position is in.
|
|
40
|
+
function diagnostic(line, programPath, rel) {
|
|
41
|
+
const m = /^(.*?):(\d+):(\d+): (.*)$/.exec(line) || /^(.*?): (.*)$/.exec(line);
|
|
42
|
+
if (!m) return { path: rel, message: printable(redact(line), 200) };
|
|
43
|
+
const [file, ln, col, text] = m.length === 5 ? [m[1], Number(m[2]), Number(m[3]), m[4]] : [m[1], null, null, m[2]];
|
|
44
|
+
const inProgram = resolve(file) === programPath;
|
|
45
|
+
return {
|
|
46
|
+
path: rel, ...(ln ? { line: ln, col } : {}), ...(inProgram ? {} : { member: printable(file, 80) }),
|
|
47
|
+
message: printable(redact(text), 200),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ironworkVersion(path, { env = process.env } = {}) {
|
|
52
|
+
const r = spawnSync(path, ['--version'], { cwd: tmpdir(), env, encoding: 'utf8', timeout: 10000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
53
|
+
return r.status === 0 ? printable(String(r.stdout).trim(), 80) || null : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Runs `ironwork check` on each program under `root`, with the tree's copy directories and the
|
|
57
|
+
// estate's copy libraries as -I, and sorts the programs into those it accepts, those it rejects,
|
|
58
|
+
// and those it could not decide: a construct it does not model, a copybook the tree lacks, or a
|
|
59
|
+
// run that ended some other way.
|
|
60
|
+
export function checkWithIronwork(path, root, { allow = null, copylibs = [], env = process.env } = {}) {
|
|
61
|
+
const programs = [...new Set(directoryTree(root).list())].filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
|
|
62
|
+
const libraries = [...buildFileIndex(root).copyDirs, ...copylibs].flatMap((d) => ['-I', d]);
|
|
63
|
+
const out = { programs: programs.length, accepted: 0, warned: 0, failed: [], notModelled: [], unresolved: [], unrun: [] };
|
|
64
|
+
for (const file of programs) {
|
|
65
|
+
const rel = relPath(root, file);
|
|
66
|
+
const r = spawnSync(path, ['check', file, ...libraries], {
|
|
67
|
+
cwd: tmpdir(), env, encoding: 'utf8', timeout: PER_PROGRAM_MS, maxBuffer: 1 << 20, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
if (COMPILED.has(r.status)) { out.accepted++; if (r.status === 4) out.warned++; continue; }
|
|
70
|
+
if (!REJECTED.has(r.status)) {
|
|
71
|
+
const why = r.error ? (r.error.code === 'ETIMEDOUT' ? `no answer in ${PER_PROGRAM_MS / 1000}s` : r.error.message) : `ironwork exited ${r.status ?? r.signal}`;
|
|
72
|
+
out.unrun.push({ path: rel, why: printable(why, 200) });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const found = String(r.stderr || '').split(/\r?\n/).filter((l) => l && !NOT_AN_ERROR.test(l)).map((l) => diagnostic(l, resolve(file), rel));
|
|
76
|
+
const errors = found.filter((d) => !notModelled(d.message) && !UNRESOLVED.test(d.message));
|
|
77
|
+
if (errors.length) out.failed.push({ ...errors[0], errors: errors.length });
|
|
78
|
+
else if (found.some((d) => UNRESOLVED.test(d.message))) out.unresolved.push(found.find((d) => UNRESOLVED.test(d.message)));
|
|
79
|
+
else out.notModelled.push(found[0] || { path: rel, message: `ironwork exited ${r.status} and gave no error` });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The compile check from a checkWithIronwork result: false where a program does not compile, null
|
|
85
|
+
// where every program that did not pass is one ironwork could not decide, true otherwise.
|
|
86
|
+
export function ironworkVerdict(result) {
|
|
87
|
+
if (result.failed.length) return false;
|
|
88
|
+
return result.notModelled.length || result.unresolved.length || result.unrun.length ? null : true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function ironworkReasons(result) {
|
|
92
|
+
const reasons = [];
|
|
93
|
+
const at = (d) => `${d.path}${d.line ? `:${d.line}:${d.col}` : ''}${d.member ? ` (in ${d.member})` : ''}`;
|
|
94
|
+
if (result.failed.length) {
|
|
95
|
+
reasons.push(`ironwork check: ${result.failed.length} of ${result.programs} program(s) do not compile as Enterprise COBOL`);
|
|
96
|
+
for (const d of result.failed.slice(0, MAX_LISTED)) reasons.push(`${at(d)}: ${d.message}`);
|
|
97
|
+
}
|
|
98
|
+
if (result.notModelled.length) reasons.push(`ironwork check: ${result.notModelled.length} program(s) use what ironwork does not model yet, so whether they compile is not decided; the first is ${at(result.notModelled[0])}: ${result.notModelled[0].message}`);
|
|
99
|
+
if (result.unresolved.length) reasons.push(`ironwork check: ${result.unresolved.length} program(s) copy a member the copy libraries do not hold; name the estate's with --copylib`);
|
|
100
|
+
if (result.unrun.length) reasons.push(`ironwork check: ${result.unrun.length} program(s) were not checked; the first, ${result.unrun[0].path}: ${result.unrun[0].why}`);
|
|
101
|
+
return reasons;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const listed = (result) => ({
|
|
105
|
+
programs: result.programs, accepted: result.accepted, warned: result.warned,
|
|
106
|
+
failed: result.failed.slice(0, MAX_LISTED), notModelled: result.notModelled.slice(0, MAX_LISTED),
|
|
107
|
+
unresolved: result.unresolved.slice(0, MAX_LISTED), unrun: result.unrun.slice(0, MAX_LISTED),
|
|
108
|
+
counts: { failed: result.failed.length, notModelled: result.notModelled.length, unresolved: result.unresolved.length, unrun: result.unrun.length },
|
|
109
|
+
});
|
package/lib/jcl.mjs
CHANGED
|
@@ -12,17 +12,14 @@
|
|
|
12
12
|
// output as `unreadable`, never dropped, because a step nobody parsed is not a step that is not
|
|
13
13
|
// there.
|
|
14
14
|
import { readSource } from './sources.mjs';
|
|
15
|
-
import { copiesOf } from './utilities.mjs';
|
|
15
|
+
import { copiesOf, tsoCommands } from './utilities.mjs';
|
|
16
|
+
import { statementCard } from './card.mjs';
|
|
16
17
|
|
|
17
18
|
// The operations a statement may carry. Anything else in the operation field is a statement this
|
|
18
19
|
// reader does not know, which is a diagnostic rather than a silent skip.
|
|
19
20
|
export const OPERATIONS = new Set(['JOB', 'EXEC', 'DD', 'PROC', 'PEND', 'SET', 'IF', 'THEN',
|
|
20
21
|
'ELSE', 'ENDIF', 'INCLUDE', 'JCLLIB', 'OUTPUT', 'CNTL', 'ENDCNTL', 'XMIT', 'COMMAND', 'NOTIFY', 'EXPORT']);
|
|
21
22
|
|
|
22
|
-
// Columns 73 to 80 are the sequence area. They are not the statement, and a payload hidden there
|
|
23
|
-
// is the hidden-content rules' business, not this reader's.
|
|
24
|
-
const STATEMENT_COLUMNS = 72;
|
|
25
|
-
|
|
26
23
|
// A continued operand resumes in these columns. Outside them it is not a continuation, whatever
|
|
27
24
|
// the previous line ended with.
|
|
28
25
|
const CONTINUE_FROM = 4;
|
|
@@ -146,8 +143,10 @@ export function foldStatements(src) {
|
|
|
146
143
|
const line = i + 1;
|
|
147
144
|
const raw = phys[i];
|
|
148
145
|
if (raw.length > 80) diags.push({ sev: 'warn', line, text: 'line is longer than 80 columns' });
|
|
149
|
-
|
|
150
|
-
const
|
|
146
|
+
// The sequence area is not the statement; a payload hidden there is the hidden-content rules' business.
|
|
147
|
+
const card = statementCard(raw);
|
|
148
|
+
const text = card.text.replace(/\s+$/, '');
|
|
149
|
+
const sequence = card.sequence;
|
|
151
150
|
|
|
152
151
|
if (stream) {
|
|
153
152
|
// A custom DLM ends the stream and nothing else does. The default ends at /* or at the next
|
|
@@ -402,6 +401,27 @@ export function parseJcl(src, file, opts = {}) {
|
|
|
402
401
|
else diags.push({ sev: 'warn', line: 0, text: `symbolic parameter &${s} has no value, so the operands using it were read unsubstituted` });
|
|
403
402
|
}
|
|
404
403
|
|
|
404
|
+
// A TSO batch step's ALLOCATE commands define DDs its JCL does not show. Each is kept as a DD of the
|
|
405
|
+
// step, marked `allocated`, with the datasets of a list concatenated under it as JCL would.
|
|
406
|
+
for (const s of steps) {
|
|
407
|
+
const tso = tsoCommands(s);
|
|
408
|
+
for (const n of tso.notes) diags.push({ sev: 'info', ...n });
|
|
409
|
+
for (const a of tso.allocations) {
|
|
410
|
+
const ends = a.datasets.length ? a.datasets : [{ written: null, dsn: null }];
|
|
411
|
+
ends.forEach((d, i) => {
|
|
412
|
+
const dd = {
|
|
413
|
+
name: i === 0 ? a.dd : null, procStep: null, dsn: d.dsn, rawDsn: d.written, referback: null,
|
|
414
|
+
disp: a.status, access: accessOf(a.status), temporary: false, sysout: a.sysout,
|
|
415
|
+
inStream: null, dlm: null, concatenated: i > 0, keywords: new Map(), line: a.line, endLine: a.line,
|
|
416
|
+
step: s.name, allocated: true,
|
|
417
|
+
};
|
|
418
|
+
dds.push(dd);
|
|
419
|
+
s.dds.push(dd);
|
|
420
|
+
stepOf.set(dd, s);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
405
425
|
// What each step's program copies, from lib/utilities.mjs. A program that table does not know
|
|
406
426
|
// copies nothing here, which is not the same as copying nothing.
|
|
407
427
|
const copies = [];
|
package/lib/kernel/findings.mjs
CHANGED
|
@@ -42,6 +42,17 @@ export const EVIDENCE = Object.freeze({
|
|
|
42
42
|
context: 'describes the estate - an entry point, a product in use - and asserts no defect',
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
// Whether an attacker can use a path finding, most urgent first. lib/exploitability.mjs decides it.
|
|
46
|
+
export const EXPLOITABILITY = Object.freeze({
|
|
47
|
+
confirmed: 'the estate reproduced it on a system it owns and brought the result as a witness feed',
|
|
48
|
+
exploitable: 'input an attacker supplies reaches the operation with no check on every route, and the estate declares an entry that carries it open to any user',
|
|
49
|
+
'attacker-driven': 'input whoever runs an entry supplies reaches the operation with no check on every route; the estate has not declared who may run that entry',
|
|
50
|
+
restricted: 'input reaches the operation with no check on every route, and every entry that carries it is behind a control the estate named',
|
|
51
|
+
mitigated: 'input reaches the operation, but a check runs first on every route, or SSRANGE turns an overrun into an abend',
|
|
52
|
+
upstream: 'the value comes from a file record or a database row, so whoever can write that data drives it',
|
|
53
|
+
refuted: 'no harmful value arrives: a check leaves only values the operation can take, or no route reaches it',
|
|
54
|
+
});
|
|
55
|
+
|
|
45
56
|
export const WHO_ACTS = Object.freeze({
|
|
46
57
|
path: "the program's owner",
|
|
47
58
|
construct: 'the owner, or whoever can rotate a credential',
|
package/lib/kernel/registry.mjs
CHANGED
|
@@ -25,6 +25,8 @@ import { scanWeb, WEB_RULES } from '../sets/web.mjs';
|
|
|
25
25
|
import { scanPriv, PRIV_RULES } from '../sets/priv.mjs';
|
|
26
26
|
import { scanLog, LOG_RULES } from '../sets/log.mjs';
|
|
27
27
|
import { scanCompile, COMPILE_RULES } from '../sets/compile.mjs';
|
|
28
|
+
import { scanSemantics, SEMANTICS_RULES } from '../sets/semantics.mjs';
|
|
29
|
+
import { scanZowe, ZOWE_RULES } from '../sets/zowe.mjs';
|
|
28
30
|
import { toolName } from './ruleset.mjs';
|
|
29
31
|
|
|
30
32
|
// The order is the order a scan runs them and the order --only lists them. It is not significant
|
|
@@ -43,6 +45,8 @@ export const REGISTRY = [
|
|
|
43
45
|
{ name: 'compile', scan: scanCompile, rules: COMPILE_RULES },
|
|
44
46
|
{ name: 'priv', scan: scanPriv, rules: PRIV_RULES },
|
|
45
47
|
{ name: 'log', scan: scanLog, rules: LOG_RULES },
|
|
48
|
+
{ name: 'semantics', scan: scanSemantics, rules: SEMANTICS_RULES },
|
|
49
|
+
{ name: 'zowe', scan: scanZowe, rules: ZOWE_RULES },
|
|
46
50
|
];
|
|
47
51
|
|
|
48
52
|
// Re-exported from the kernel's ruleset module, which is where it has to live: this file imports
|
package/lib/layout.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// How a data description is laid out in storage: the bytes a PICTURE and USAGE take, where each item
|
|
3
|
+
// starts within its record, and how wide a report group prints. Items are { level, occurs, redefines,
|
|
4
|
+
// sync, children, ... } as lib/parser.mjs builds them.
|
|
5
|
+
|
|
6
|
+
// The binary sizes a dialect gives COMP by digit count.
|
|
7
|
+
export const BINARY_SIZE = { default: '1-2-4-8', ibm: '2-4-8', mf: '1--8' };
|
|
8
|
+
|
|
9
|
+
function picInfo(pic, constants) {
|
|
10
|
+
const info = { digits: 0, display: 0, signed: false, alphanumeric: false, national: false };
|
|
11
|
+
const re = /(.)\(([A-Za-z0-9_-]+)\)|(.)/g;
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(pic))) {
|
|
14
|
+
const ch = (m[1] || m[3]).toUpperCase();
|
|
15
|
+
let n = 1;
|
|
16
|
+
if (m[1]) {
|
|
17
|
+
const raw = m[2];
|
|
18
|
+
n = /^\d+$/.test(raw) ? Number(raw) : Number(constants && constants.get(raw.toUpperCase()));
|
|
19
|
+
if (!Number.isFinite(n) || n < 0) n = 0;
|
|
20
|
+
}
|
|
21
|
+
if (ch === 'S') { info.signed = true; continue; }
|
|
22
|
+
if (ch === 'V' || ch === 'P') continue;
|
|
23
|
+
if (ch === '9') { info.digits += n; info.display += n; continue; }
|
|
24
|
+
if (ch === 'N' || ch === 'G') { info.national = true; info.display += 2 * n; continue; }
|
|
25
|
+
if (ch === 'X' || ch === 'A') info.alphanumeric = true;
|
|
26
|
+
info.display += n;
|
|
27
|
+
}
|
|
28
|
+
return info;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Storage bytes of a literal: hexadecimal literals hold one byte per two digits, a Z literal adds a
|
|
32
|
+
// terminating null, a national literal holds two bytes per character.
|
|
33
|
+
export function literalBytes(tok) {
|
|
34
|
+
const prefix = tok.prefix || '';
|
|
35
|
+
if (prefix === 'X' || prefix === 'BX' || prefix === 'NX') return Math.floor(tok.v.length / (prefix === 'NX' ? 4 : 2)) * (prefix === 'NX' ? 2 : 1);
|
|
36
|
+
if (prefix === 'Z') return tok.v.length + 1;
|
|
37
|
+
if (prefix === 'N' || prefix === 'NC' || prefix === 'U') return tok.v.length * 2;
|
|
38
|
+
return tok.v.length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function binaryBytes(digits, scheme) {
|
|
42
|
+
if (scheme === '2-4-8') return digits <= 4 ? 2 : digits <= 9 ? 4 : 8;
|
|
43
|
+
if (scheme === '1--8') return [1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8][Math.min(digits, 18)] || 8;
|
|
44
|
+
return digits <= 2 ? 1 : digits <= 4 ? 2 : digits <= 9 ? 4 : 8;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function elementarySize(item, scheme, constants) {
|
|
48
|
+
if (!item.picture && !item.usage) {
|
|
49
|
+
const constBytes = constants && constants.textBytes;
|
|
50
|
+
const fromValue = item.values.reduce((n, v) => n + (v.t === 'lit' ? literalBytes(v) : v.t === 'word' && constBytes && constBytes.has(v.u) ? constBytes.get(v.u) : 0), 0);
|
|
51
|
+
if (item.section === 'SCREEN' && !fromValue && item.screenRefItem) return item.screenRefItem.size || 0;
|
|
52
|
+
if (item.section === 'SCREEN') return Math.max(1, fromValue);
|
|
53
|
+
if (fromValue) return fromValue;
|
|
54
|
+
}
|
|
55
|
+
if (item.section === 'SCREEN' && !item.picture) return 1;
|
|
56
|
+
const usage = item.effectiveUsage || 'DISPLAY';
|
|
57
|
+
const p = item.picture ? picInfo(item.picture, constants) : null;
|
|
58
|
+
const digits = p ? p.digits : 0;
|
|
59
|
+
const u = usage.replace('COMPUTATIONAL', 'COMP');
|
|
60
|
+
if (u === 'COMP-1' || u === 'FLOAT-SHORT') return 4;
|
|
61
|
+
if (u === 'COMP-2' || u === 'FLOAT-LONG' || u === 'FLOAT-DECIMAL-16') return 8;
|
|
62
|
+
if (u === 'FLOAT-DECIMAL-34') return 16;
|
|
63
|
+
if (u === 'INDEX') return 4;
|
|
64
|
+
if (u === 'POINTER' || u === 'PROGRAM-POINTER' || u === 'FUNCTION-POINTER' || u === 'PROCEDURE-POINTER') return 8;
|
|
65
|
+
if (u === 'BINARY-CHAR') return 1;
|
|
66
|
+
if (u === 'BINARY-SHORT' || u === 'SIGNED-SHORT' || u === 'UNSIGNED-SHORT') return 2;
|
|
67
|
+
if (u === 'BINARY-LONG' || u === 'BINARY-INT' || u === 'SIGNED-INT' || u === 'UNSIGNED-INT') return 4;
|
|
68
|
+
if (u === 'BINARY-DOUBLE' || u === 'BINARY-LONG-LONG' || u === 'BINARY-C-LONG' || u === 'SIGNED-LONG' || u === 'UNSIGNED-LONG') return 8;
|
|
69
|
+
if (u === 'COMP-3' || u === 'PACKED-DECIMAL') return Math.floor(digits / 2) + 1;
|
|
70
|
+
if (u === 'COMP-6') return Math.ceil(digits / 2);
|
|
71
|
+
if (u === 'COMP-X' || u === 'COMP-N' || (u === 'COMP-5' && p && p.alphanumeric)) {
|
|
72
|
+
if (p && p.alphanumeric) return p.display;
|
|
73
|
+
return Math.max(1, Math.ceil((digits * Math.log(10)) / Math.log(256)));
|
|
74
|
+
}
|
|
75
|
+
if (u === 'COMP' || u === 'COMP-4' || u === 'COMP-5' || u === 'BINARY') return binaryBytes(digits, scheme);
|
|
76
|
+
let len = p ? p.display : 0;
|
|
77
|
+
if (item.signSeparate && p && p.signed) len++;
|
|
78
|
+
return len;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// SYNCHRONIZED aligns a binary, floating-point, pointer or index item to its own length; packed,
|
|
82
|
+
// COMP-X and display items are not moved.
|
|
83
|
+
const ALIGNED_USAGE = /^(COMP|COMP-[1245]|BINARY(-[A-Z-]+)?|FLOAT-[A-Z0-9-]+|(PROGRAM-|FUNCTION-|PROCEDURE-)?POINTER|INDEX|(UN)?SIGNED-[A-Z]+)$/;
|
|
84
|
+
|
|
85
|
+
export function computeSizes(roots, scheme, constants) {
|
|
86
|
+
// `at` is where the item starts, counted from the start of its record, because the compiler aligns
|
|
87
|
+
// a SYNCHRONIZED item against the record and puts the slack inside the group that holds it. A
|
|
88
|
+
// table's entry is laid out from its own start and rounded up to its widest alignment, so every
|
|
89
|
+
// occurrence aligns alike.
|
|
90
|
+
const visit = (item, inheritedUsage, inheritedSignSeparate, at) => {
|
|
91
|
+
item.effectiveUsage = item.usage || inheritedUsage || null;
|
|
92
|
+
if (inheritedSignSeparate && !item.signExplicit) item.signSeparate = true;
|
|
93
|
+
const structural = item.children.filter(c => c.level !== 88 && c.level !== 66 && c.level !== 78);
|
|
94
|
+
if (!structural.length) {
|
|
95
|
+
const one = elementarySize(item, scheme, constants);
|
|
96
|
+
item.contributes = one * item.occurs;
|
|
97
|
+
// The listing prints the whole table only for POINTER and INDEX; every other usage prints one occurrence.
|
|
98
|
+
const wholeTable = /^(POINTER|INDEX)$/.test(item.effectiveUsage || '');
|
|
99
|
+
item.size = wholeTable ? item.contributes : one;
|
|
100
|
+
const usage = (item.effectiveUsage || '').replace('COMPUTATIONAL', 'COMP');
|
|
101
|
+
item.align = item.sync && ALIGNED_USAGE.test(usage) ? Math.min(one, 8) : 1;
|
|
102
|
+
item.maxAlign = item.align;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const base = item.occurs > 1 ? 0 : at;
|
|
106
|
+
let offset = 0;
|
|
107
|
+
let end = 0;
|
|
108
|
+
let maxAlign = 1;
|
|
109
|
+
const startOf = new Map();
|
|
110
|
+
for (const c of structural) {
|
|
111
|
+
if (c.redefines) {
|
|
112
|
+
// REDEFINES names a SIBLING. Resolving it by name across the whole program reached the
|
|
113
|
+
// first item of that name anywhere, which crossed records.
|
|
114
|
+
const known = startOf.has(c.redefines);
|
|
115
|
+
visit(c, item.effectiveUsage, item.signSeparate, base + (known ? startOf.get(c.redefines) : offset));
|
|
116
|
+
const start = known ? startOf.get(c.redefines) : offset - c.contributes;
|
|
117
|
+
startOf.set(c.name, start);
|
|
118
|
+
c.localStart = start;
|
|
119
|
+
end = Math.max(end, start + c.contributes);
|
|
120
|
+
// A REDEFINES larger than the item it redefines pushes the next sibling past its end.
|
|
121
|
+
offset = Math.max(offset, start + c.contributes);
|
|
122
|
+
maxAlign = Math.max(maxAlign, c.maxAlign);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
visit(c, item.effectiveUsage, item.signSeparate, base + offset);
|
|
126
|
+
const skew = (base + offset) % c.align;
|
|
127
|
+
if (c.align > 1 && skew) offset += c.align - skew;
|
|
128
|
+
startOf.set(c.name, offset);
|
|
129
|
+
c.localStart = offset;
|
|
130
|
+
offset += c.contributes;
|
|
131
|
+
end = Math.max(end, offset);
|
|
132
|
+
maxAlign = Math.max(maxAlign, c.maxAlign);
|
|
133
|
+
}
|
|
134
|
+
if (item.occurs > 1 && end % maxAlign) end += maxAlign - (end % maxAlign);
|
|
135
|
+
item.size = end * item.occurs;
|
|
136
|
+
item.contributes = item.size;
|
|
137
|
+
item.align = 1;
|
|
138
|
+
item.maxAlign = maxAlign;
|
|
139
|
+
};
|
|
140
|
+
for (const r of roots) visit(r, null, false, 0);
|
|
141
|
+
// Offsets are assigned after sizing: a child's absolute start depends on its parent's, which is
|
|
142
|
+
// only known once the parent's own siblings have been laid out.
|
|
143
|
+
const place = (item, at) => {
|
|
144
|
+
item.offset = at;
|
|
145
|
+
for (const c of item.children) if (c.localStart != null) place(c, at + c.localStart);
|
|
146
|
+
};
|
|
147
|
+
for (const r of roots) place(r, 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A report group is laid out by column, not by adding up its items: a line is as wide as the column
|
|
151
|
+
// its rightmost item ends in, COLUMN PLUS counting on from the end of the item before. A group holds
|
|
152
|
+
// its lines one after another, and every 01 group of a report, like the file the report is written
|
|
153
|
+
// to, is as large as the largest group.
|
|
154
|
+
export function layoutReport(rd) {
|
|
155
|
+
const structural = (x) => x.children.filter(c => c.level !== 88 && c.level !== 66 && c.level !== 78);
|
|
156
|
+
const mark = (x) => { x.rd = rd; for (const c of x.children) mark(c); };
|
|
157
|
+
for (const g of rd.groups) mark(g);
|
|
158
|
+
let width = 0;
|
|
159
|
+
// Items printed at the same column under PRESENT WHEN each keep their own storage, so a line is
|
|
160
|
+
// never smaller than its items laid end to end.
|
|
161
|
+
const lineWidth = (line) => {
|
|
162
|
+
let last = 0;
|
|
163
|
+
let right = 0;
|
|
164
|
+
let total = 0;
|
|
165
|
+
const place = (c) => {
|
|
166
|
+
const len = c.contributes || c.size || 0;
|
|
167
|
+
const start = c.rwColumn ? (c.rwColumn.at != null ? c.rwColumn.at : last + c.rwColumn.plus) : last + 1;
|
|
168
|
+
last = start + len - 1;
|
|
169
|
+
right = Math.max(right, last);
|
|
170
|
+
total += len;
|
|
171
|
+
};
|
|
172
|
+
const walk = (x) => { for (const c of structural(x)) { if (structural(c).length) walk(c); else place(c); } };
|
|
173
|
+
if (structural(line).length) walk(line);
|
|
174
|
+
else place(line);
|
|
175
|
+
return Math.max(right, total);
|
|
176
|
+
};
|
|
177
|
+
// The entries that open a line; a group with no LINE clause anywhere in it is one line.
|
|
178
|
+
for (const g of rd.groups) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
const collect = (x) => { if (x.rwLine) { lines.push(x); return; } for (const c of structural(x)) collect(c); };
|
|
181
|
+
collect(g);
|
|
182
|
+
if (!lines.length) lines.push(g);
|
|
183
|
+
let total = 0;
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
const w = lineWidth(line);
|
|
186
|
+
if (line !== g) { line.size = w; line.contributes = w * (line.occurs || 1); }
|
|
187
|
+
total += w;
|
|
188
|
+
}
|
|
189
|
+
width = Math.max(width, total);
|
|
190
|
+
}
|
|
191
|
+
for (const g of rd.groups) { g.size = width; g.contributes = width; }
|
|
192
|
+
rd.width = width;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Whether an item's bytes are read as decimal digits: zoned (DISPLAY with a numeric picture) or
|
|
196
|
+
// packed. A group, an edited picture, binary and floating point are not.
|
|
197
|
+
export function holdsDecimal(it) {
|
|
198
|
+
if ((it.children || []).some((c) => c.level !== 88)) return false;
|
|
199
|
+
const usage = String(it.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
|
|
200
|
+
if (usage === 'COMP-3' || usage === 'PACKED-DECIMAL' || usage === 'COMP-6') return true;
|
|
201
|
+
return usage === 'DISPLAY' && !!it.picture && /9/.test(it.picture) && /^[S9VP()0-9]+$/i.test(it.picture);
|
|
202
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Which compiler options a change to a program's CBL and PROCESS cards changes, for the families
|
|
3
|
+
// that change arithmetic, validation or collation without touching a statement
|
|
4
|
+
// (docs/spec/evidence.md §13.4). Spellings come from IBM's option table (lib/enterprise-options.mjs).
|
|
5
|
+
import { OPTION_SPELLINGS } from './enterprise-options.mjs';
|
|
6
|
+
import { optionCards, optionTokens } from './options.mjs';
|
|
7
|
+
|
|
8
|
+
export const WATCHED = ['TRUNC', 'ARITH', 'NUMPROC', 'NUMCHECK', 'SSRANGE', 'CODEPAGE', 'INTDATE'];
|
|
9
|
+
const ARITH_SUBS = { C: 'COMPAT', COMPAT: 'COMPAT', E: 'EXTEND', EXTEND: 'EXTEND' };
|
|
10
|
+
|
|
11
|
+
// One option token as `FAMILY(SUB,...)` or `NOFAMILY`, or null for a family not watched.
|
|
12
|
+
function settingOf(token) {
|
|
13
|
+
const m = /^([A-Z0-9-]+)(?:\((.*)\))?$/.exec(token);
|
|
14
|
+
if (!m) return null;
|
|
15
|
+
const spelled = OPTION_SPELLINGS[m[1]];
|
|
16
|
+
if (!spelled || !WATCHED.includes(spelled[0])) return null;
|
|
17
|
+
const [family, off] = spelled;
|
|
18
|
+
if (off) return { family, value: `NO${family}` };
|
|
19
|
+
let subs = m[2] === undefined ? [] : optionTokens(m[2]);
|
|
20
|
+
if (family === 'ARITH') subs = subs.map((s) => ARITH_SUBS[s] || s);
|
|
21
|
+
if (family === 'NUMCHECK' || family === 'SSRANGE') subs = [...subs].sort();
|
|
22
|
+
return { family, value: subs.length ? `${family}(${subs.join(',')})` : family };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The watched options a program's cards set, the later card winning, each with its line.
|
|
26
|
+
export function optionsInForce(text) {
|
|
27
|
+
const state = new Map();
|
|
28
|
+
for (const card of optionCards(text)) {
|
|
29
|
+
for (const token of card.options) {
|
|
30
|
+
const s = settingOf(token);
|
|
31
|
+
if (s) state.set(s.family, { value: s.value, line: card.line });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function optionChanges(baseText, headText) {
|
|
38
|
+
const before = optionsInForce(baseText);
|
|
39
|
+
const after = optionsInForce(headText);
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const family of WATCHED) {
|
|
42
|
+
const b = before.get(family);
|
|
43
|
+
const a = after.get(family);
|
|
44
|
+
const base = b ? b.value : 'default';
|
|
45
|
+
const head = a ? a.value : 'default';
|
|
46
|
+
if (base !== head) out.push({ option: family, base, head, line: (a || b).line });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|