@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
package/lib/ironwork.mjs
CHANGED
|
@@ -19,17 +19,20 @@ const REJECTED = new Set([8, 12, 16]);
|
|
|
19
19
|
// errors in the program, whatever their wording says; the wording decides only at 12 and 16.
|
|
20
20
|
const GRADED_ERROR = 8;
|
|
21
21
|
// Warnings and informational lines follow the errors, and say nothing about why a program failed.
|
|
22
|
-
|
|
22
|
+
// The label follows the position, or opens a line that has none.
|
|
23
|
+
const NOT_AN_ERROR = /(?:^|: )(?:warning|informational): /;
|
|
24
|
+
// ironwork opens a message with its id and severity, `IWC0101-S`, as IBM opens its own.
|
|
25
|
+
const MESSAGE_ID = /^((?:IW[A-Z]|IGY[A-Z]{2})\d{4})-[IWESU] (.*)$/;
|
|
23
26
|
const PER_PROGRAM_MS = 60000;
|
|
24
27
|
const MAX_LISTED = 10;
|
|
25
28
|
|
|
26
|
-
// ironwork refuses by name what it does not model yet; that says nothing about the
|
|
27
|
-
// the CICS, DL/I or SQL translator declares it reports as undefined, without saying
|
|
28
|
-
// read as its gap, not the program's.
|
|
29
|
+
// ironwork refuses by name what it does not model yet, with an IWR id; that says nothing about the
|
|
30
|
+
// program. A name the CICS, DL/I or SQL translator declares it reports as undefined, without saying
|
|
31
|
+
// so, and that is read as its gap, not the program's.
|
|
29
32
|
const NOT_SUPPORTED = /\bnot supported\b/i;
|
|
30
33
|
const TRANSLATOR_NAMES = [EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS];
|
|
31
|
-
function notModelled(message) {
|
|
32
|
-
if (NOT_SUPPORTED.test(message)) return true;
|
|
34
|
+
function notModelled({ id, message }) {
|
|
35
|
+
if (id?.startsWith('IWR') || NOT_SUPPORTED.test(message)) return true;
|
|
33
36
|
const name = /^([A-Z][A-Z0-9-]*) is not defined$/.exec(message);
|
|
34
37
|
return !!name && TRANSLATOR_NAMES.some((s) => s.has(name[1]));
|
|
35
38
|
}
|
|
@@ -45,9 +48,10 @@ function diagnostic(line, programPath, rel) {
|
|
|
45
48
|
if (!m) return { path: rel, message: printable(redact(line), 200) };
|
|
46
49
|
const [file, ln, col, text] = m.length === 5 ? [m[1], Number(m[2]), Number(m[3]), m[4]] : [m[1], null, null, m[2]];
|
|
47
50
|
const inProgram = resolve(file) === programPath;
|
|
51
|
+
const [, id, said] = MESSAGE_ID.exec(text) || [null, null, text];
|
|
48
52
|
return {
|
|
49
|
-
path: rel, ...(ln ? { line: ln, col } : {}), ...(inProgram ? {} : { member: printable(file, 80) }),
|
|
50
|
-
message: printable(redact(
|
|
53
|
+
path: rel, ...(ln ? { line: ln, col } : {}), ...(inProgram ? {} : { member: printable(file, 80) }), ...(id ? { id } : {}),
|
|
54
|
+
message: printable(redact(said), 200),
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -60,9 +64,9 @@ export function ironworkVersion(path, { env = process.env } = {}) {
|
|
|
60
64
|
// estate's copy libraries as -I, and sorts the programs into those it accepts, those it rejects,
|
|
61
65
|
// and those it could not decide: a construct it does not model, a copybook the tree lacks, or a
|
|
62
66
|
// run that ended some other way.
|
|
63
|
-
export function checkWithIronwork(path, root, { allow = null, copylibs = [], env = process.env } = {}) {
|
|
64
|
-
const programs = [...new Set(directoryTree(root).list())].filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
|
|
65
|
-
const libraries = [...buildFileIndex(root).copyDirs, ...copylibs].flatMap((d) => ['-I', d]);
|
|
67
|
+
export function checkWithIronwork(path, root, { allow = null, programs: given = null, copyDirs = null, copylibs = [], env = process.env } = {}) {
|
|
68
|
+
const programs = given || [...new Set(directoryTree(root).list())].filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
|
|
69
|
+
const libraries = [...(copyDirs || buildFileIndex(root).copyDirs), ...copylibs].flatMap((d) => ['-I', d]);
|
|
66
70
|
const out = { programs: programs.length, accepted: 0, warned: 0, failed: [], notModelled: [], unresolved: [], unrun: [] };
|
|
67
71
|
for (const file of programs) {
|
|
68
72
|
const rel = relPath(root, file);
|
|
@@ -76,7 +80,7 @@ export function checkWithIronwork(path, root, { allow = null, copylibs = [], env
|
|
|
76
80
|
continue;
|
|
77
81
|
}
|
|
78
82
|
const found = String(r.stderr || '').split(/\r?\n/).filter((l) => l && !NOT_AN_ERROR.test(l)).map((l) => diagnostic(l, resolve(file), rel));
|
|
79
|
-
const errors = found.filter((d) => !(r.status !== GRADED_ERROR && notModelled(d
|
|
83
|
+
const errors = found.filter((d) => !(r.status !== GRADED_ERROR && notModelled(d)) && !UNRESOLVED.test(d.message));
|
|
80
84
|
if (errors.length) out.failed.push({ ...errors[0], errors: errors.length });
|
|
81
85
|
else if (found.some((d) => UNRESOLVED.test(d.message))) out.unresolved.push(found.find((d) => UNRESOLVED.test(d.message)));
|
|
82
86
|
else out.notModelled.push(found[0] || { path: rel, message: `ironwork exited ${r.status} and gave no error` });
|
|
@@ -93,7 +97,7 @@ export function ironworkVerdict(result) {
|
|
|
93
97
|
|
|
94
98
|
export function ironworkReasons(result) {
|
|
95
99
|
const reasons = [];
|
|
96
|
-
const at = (d) => `${d.path}${d.line ? `:${d.line}:${d.col}` : ''}${d.member ? ` (in ${d.member})` : ''}`;
|
|
100
|
+
const at = (d) => `${d.path}${d.line ? `:${d.line}:${d.col}` : ''}${d.member ? ` (in ${d.member})` : ''}${d.id ? ` ${d.id}` : ''}`;
|
|
97
101
|
if (result.failed.length) {
|
|
98
102
|
reasons.push(`ironwork check: ${result.failed.length} of ${result.programs} program(s) do not compile as Enterprise COBOL`);
|
|
99
103
|
for (const d of result.failed.slice(0, MAX_LISTED)) reasons.push(`${at(d)}: ${d.message}`);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Data sets brought off z/OS whole: a TSO TRANSMIT (XMIT) file, and an IEBCOPY unload data set, alone
|
|
3
|
+
// or inside one. Each partitioned data set's members come back as their records, in EBCDIC, ended by
|
|
4
|
+
// NL (X'15'), as a z/OSMF record download gives them; a sequential data set comes back as one file.
|
|
5
|
+
//
|
|
6
|
+
// The layouts are IBM's: z/OS TSO/E Customization, "Format of transmitted data", "Control record
|
|
7
|
+
// formats" and "Numeric values" (the text units); z/OS DFSMSdfp Utilities, "Unload partitioned data
|
|
8
|
+
// set format" (COPYR1, COPYR2, directory and member data records); z/OS DFSMSdfp Advanced Services,
|
|
9
|
+
// "Data Extent Block (DEB) Fields" and "Device Characteristics Information" (DEVTYPE), which turn a
|
|
10
|
+
// member block's MBBCCHHR into the TTR its directory entry holds.
|
|
11
|
+
import { ebcdicByte } from '../sources.mjs';
|
|
12
|
+
|
|
13
|
+
const EOR = 0x15;
|
|
14
|
+
const UNLOAD_ID = [0xca, 0x6d, 0x0f];
|
|
15
|
+
const ebcdic = (s) => Buffer.from([...s].map((c) => ebcdicByte(c)));
|
|
16
|
+
const INMR = (n) => ebcdic(`INMR0${n}`);
|
|
17
|
+
|
|
18
|
+
const KEY = { INMDSNAM: 0x0002, INMDSORG: 0x003c, INMLRECL: 0x0042, INMRECFM: 0x0049, INMUTILN: 0x1028, INMNUMF: 0x102f };
|
|
19
|
+
const DSORG_PO = 0x0200;
|
|
20
|
+
|
|
21
|
+
const textOf = (buf) => [...buf].map((b) => EBCDIC_TEXT[b]).join('');
|
|
22
|
+
const EBCDIC_TEXT = (() => {
|
|
23
|
+
const t = new Array(256).fill('?');
|
|
24
|
+
for (let c = 0x20; c < 0x7f; c++) {
|
|
25
|
+
const b = ebcdicByte(String.fromCharCode(c));
|
|
26
|
+
if (b !== null) t[b] = String.fromCharCode(c);
|
|
27
|
+
}
|
|
28
|
+
return t;
|
|
29
|
+
})();
|
|
30
|
+
const uint = (buf) => buf.reduce((n, b) => n * 256 + b, 0);
|
|
31
|
+
const isUnloadId = (buf, at) => buf.length >= at + 3 && UNLOAD_ID.every((b, i) => buf[at + i] === b);
|
|
32
|
+
|
|
33
|
+
// What a file holds, from its first bytes: an XMIT file's first segment is its INMR01 header record;
|
|
34
|
+
// an unload's COPYR1 carries X'CA6D0F' after its flag byte, with a BDW and SDW, an RDW, or nothing
|
|
35
|
+
// before it.
|
|
36
|
+
export function archiveKind(head) {
|
|
37
|
+
if (head.length >= 8 && (head[1] & 0xa0) === 0xa0 && head.subarray(2, 8).equals(INMR(1))) return 'xmit';
|
|
38
|
+
for (const at of [1, 5, 9]) if (isUnloadId(head, at)) return 'unload';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The transmission's logical records, each from its segments: a length byte counting itself and the
|
|
43
|
+
// flag byte, the flags (X'80' first, X'40' last, X'20' control record) and up to 253 bytes.
|
|
44
|
+
function xmitRecords(buf) {
|
|
45
|
+
const records = [];
|
|
46
|
+
let parts = null;
|
|
47
|
+
let control = false;
|
|
48
|
+
for (let at = 0; at < buf.length;) {
|
|
49
|
+
const n = buf[at];
|
|
50
|
+
if (n < 2 || at + n > buf.length) break;
|
|
51
|
+
const flags = buf[at + 1];
|
|
52
|
+
if (flags & 0x80) { parts = []; control = (flags & 0x20) !== 0; }
|
|
53
|
+
if (parts) parts.push(buf.subarray(at + 2, at + n));
|
|
54
|
+
if (flags & 0x40 && parts) {
|
|
55
|
+
const bytes = Buffer.concat(parts);
|
|
56
|
+
records.push({ control, bytes });
|
|
57
|
+
parts = null;
|
|
58
|
+
if (control && bytes.subarray(0, 6).equals(INMR(6))) break;
|
|
59
|
+
}
|
|
60
|
+
at += n;
|
|
61
|
+
}
|
|
62
|
+
return records;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Text units from `at`: a key, a count, and that many length and data pairs.
|
|
66
|
+
function textUnits(bytes, at) {
|
|
67
|
+
const units = new Map();
|
|
68
|
+
while (at + 4 <= bytes.length) {
|
|
69
|
+
const key = bytes.readUInt16BE(at);
|
|
70
|
+
const count = bytes.readUInt16BE(at + 2);
|
|
71
|
+
at += 4;
|
|
72
|
+
const fields = [];
|
|
73
|
+
for (let i = 0; i < count && at + 2 <= bytes.length; i++) {
|
|
74
|
+
const len = bytes.readUInt16BE(at);
|
|
75
|
+
fields.push(bytes.subarray(at + 2, at + 2 + len));
|
|
76
|
+
at += 2 + len;
|
|
77
|
+
}
|
|
78
|
+
units.set(key, fields);
|
|
79
|
+
}
|
|
80
|
+
return units;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readXmit(buf) {
|
|
84
|
+
const records = xmitRecords(buf);
|
|
85
|
+
const last = records[records.length - 1];
|
|
86
|
+
if (!last?.control || !last.bytes.subarray(0, 6).equals(INMR(6))) return { why: 'the transmission ends before its INMR06 trailer' };
|
|
87
|
+
const header = textUnits(records[0].bytes, 6);
|
|
88
|
+
const files = header.get(KEY.INMNUMF)?.[0];
|
|
89
|
+
if (files && uint(files) > 1) return { why: `the transmission holds ${uint(files)} files, and cobolwork reads one` };
|
|
90
|
+
const utilities = records.filter((r) => r.control && r.bytes.subarray(0, 6).equals(INMR(2))).map((r) => textUnits(r.bytes, 10));
|
|
91
|
+
const named = utilities.find((u) => u.has(KEY.INMDSNAM));
|
|
92
|
+
const dataSet = named ? named.get(KEY.INMDSNAM).map(textOf).join('.') : null;
|
|
93
|
+
const start = records.findIndex((r) => r.control && r.bytes.subarray(0, 6).equals(INMR(3)));
|
|
94
|
+
if (start < 0) return { why: 'the transmission has no INMR03 data control record' };
|
|
95
|
+
const data = records.slice(start + 1, -1).filter((r) => !r.control).map((r) => r.bytes);
|
|
96
|
+
const unloaded = utilities.some((u) => textOf(u.get(KEY.INMUTILN)?.[0] || Buffer.alloc(0)) === 'IEBCOPY');
|
|
97
|
+
if (unloaded) return { ...readUnloadRecords(data), kind: 'xmit', dataSet };
|
|
98
|
+
const dsorg = utilities.map((u) => u.get(KEY.INMDSORG)?.[0]).find(Boolean);
|
|
99
|
+
if (dsorg && uint(dsorg) === DSORG_PO) return { why: 'a partitioned data set transmitted without IEBCOPY' };
|
|
100
|
+
return { kind: 'xmit', dataSet, sequential: true, members: [{ name: null, bytes: joined(data) }], aliases: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// An unload stored with its descriptors: each block a BDW, then segments, each an SDW whose third
|
|
104
|
+
// byte's low two bits say whether it is a whole record (0), its first (1), last (2) or middle (3)
|
|
105
|
+
// part; or each record after an RDW; or the records with nothing between them, COPYR1 and COPYR2 at
|
|
106
|
+
// their fixed lengths and everything after them one stream.
|
|
107
|
+
function unloadRecords(buf) {
|
|
108
|
+
if (isUnloadId(buf, 9)) {
|
|
109
|
+
const records = [];
|
|
110
|
+
let parts = [];
|
|
111
|
+
for (let at = 0; at + 4 <= buf.length;) {
|
|
112
|
+
const end = at + buf.readUInt16BE(at);
|
|
113
|
+
if (end <= at || end > buf.length) break;
|
|
114
|
+
for (let s = at + 4; s + 4 <= end;) {
|
|
115
|
+
const len = buf.readUInt16BE(s);
|
|
116
|
+
if (len < 4) break;
|
|
117
|
+
const code = buf[s + 2] & 0x03;
|
|
118
|
+
parts.push(buf.subarray(s + 4, s + len));
|
|
119
|
+
if (code === 0 || code === 2) { records.push(Buffer.concat(parts)); parts = []; }
|
|
120
|
+
s += len;
|
|
121
|
+
}
|
|
122
|
+
at = end;
|
|
123
|
+
}
|
|
124
|
+
return records;
|
|
125
|
+
}
|
|
126
|
+
if (isUnloadId(buf, 5)) {
|
|
127
|
+
const records = [];
|
|
128
|
+
for (let at = 0; at + 4 <= buf.length;) {
|
|
129
|
+
const len = buf.readUInt16BE(at);
|
|
130
|
+
if (len < 4 || at + len > buf.length) break;
|
|
131
|
+
records.push(buf.subarray(at + 4, at + len));
|
|
132
|
+
at += len;
|
|
133
|
+
}
|
|
134
|
+
return records;
|
|
135
|
+
}
|
|
136
|
+
return [buf.subarray(0, COPYR1_LENGTH), buf.subarray(COPYR1_LENGTH, COPYR1_LENGTH + COPYR2_LENGTH), buf.subarray(COPYR1_LENGTH + COPYR2_LENGTH)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const COPYR1_LENGTH = 56;
|
|
140
|
+
const COPYR2_LENGTH = 276;
|
|
141
|
+
const DIRECTORY_BLOCK = 276;
|
|
142
|
+
const BLOCK_HEADER = 12;
|
|
143
|
+
|
|
144
|
+
export function readArchive(buf) {
|
|
145
|
+
const kind = archiveKind(buf.subarray(0, 16));
|
|
146
|
+
if (kind === 'xmit') return readXmit(buf);
|
|
147
|
+
if (kind === 'unload') return { ...readUnloadRecords(unloadRecords(buf)), kind: 'unload', dataSet: null };
|
|
148
|
+
return { why: 'neither an XMIT file nor an IEBCOPY unload' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// COPYR1 and COPYR2, then the directory blocks and every member's blocks, which run on across
|
|
152
|
+
// records: a directory block is a 12-byte count, the 8-byte key and 256 bytes of entries, and the
|
|
153
|
+
// directory ends with 12 bytes of zeros; a member's blocks are each a flag byte, MBB, CCHHR, key
|
|
154
|
+
// length, data length, key and data, and its last is one with no key and no data.
|
|
155
|
+
function readUnloadRecords(records) {
|
|
156
|
+
const [r1, r2] = records;
|
|
157
|
+
if (!r1 || !isUnloadId(r1, 1)) return { why: 'the unload has no COPYR1 record' };
|
|
158
|
+
const format = r1[0] >> 6;
|
|
159
|
+
if (format === 2) return { why: 'IEBCOPY marked the unload incomplete or in error' };
|
|
160
|
+
if (format !== 0 || r1[0] & 0x01) return { why: 'a PDSE unload, whose attribute records cobolwork does not read yet' };
|
|
161
|
+
const headers = r1.length >= 38 ? r1.readUInt16BE(36) || 2 : 2;
|
|
162
|
+
if (headers !== 2) return { why: `the unload has ${headers} header records, and cobolwork reads two` };
|
|
163
|
+
if (!r2 || r2.length < 16 + 16) return { why: 'the unload has no COPYR2 record' };
|
|
164
|
+
const lrecl = r1.readUInt16BE(8);
|
|
165
|
+
const recfm = r1[10] >> 6;
|
|
166
|
+
const tracksPerCylinder = r1.readUInt16BE(26);
|
|
167
|
+
const extents = [];
|
|
168
|
+
for (let at = 16; at + 16 <= r2.length && extents.length < 16; at += 16) {
|
|
169
|
+
extents.push({ cyl: cylinder(r2.readUInt16BE(at + 6), r2.readUInt16BE(at + 8)), trk: r2.readUInt16BE(at + 8) & 0x0f, tracks: r2[at + 5] * 0x10000 + r2.readUInt16BE(at + 14) });
|
|
170
|
+
}
|
|
171
|
+
const stream = Buffer.concat(records.slice(2));
|
|
172
|
+
let at = 0;
|
|
173
|
+
const entries = [];
|
|
174
|
+
for (;;) {
|
|
175
|
+
if (at + BLOCK_HEADER > stream.length) return { why: 'the directory runs past the end of the unload' };
|
|
176
|
+
if (stream.subarray(at, at + BLOCK_HEADER).every((b) => b === 0)) { at += BLOCK_HEADER; break; }
|
|
177
|
+
const block = stream.subarray(at + BLOCK_HEADER + 8, at + DIRECTORY_BLOCK);
|
|
178
|
+
const used = Math.min(block.readUInt16BE(0), block.length);
|
|
179
|
+
for (let e = 2; e + 12 <= used;) {
|
|
180
|
+
const name = block.subarray(e, e + 8);
|
|
181
|
+
if (name.every((b) => b === 0xff)) break;
|
|
182
|
+
const c = block[e + 11];
|
|
183
|
+
entries.push({ name: textOf(name).trimEnd(), ttr: uint(block.subarray(e + 8, e + 11)), alias: (c & 0x80) !== 0 });
|
|
184
|
+
e += 12 + (c & 0x1f) * 2;
|
|
185
|
+
}
|
|
186
|
+
at += DIRECTORY_BLOCK;
|
|
187
|
+
}
|
|
188
|
+
const byTtr = new Map(entries.filter((e) => !e.alias).map((e) => [e.ttr, e.name]));
|
|
189
|
+
const members = [];
|
|
190
|
+
let unmatched = 0;
|
|
191
|
+
while (at + BLOCK_HEADER <= stream.length) {
|
|
192
|
+
let ttr = null;
|
|
193
|
+
const blocks = [];
|
|
194
|
+
for (;;) {
|
|
195
|
+
if (at + BLOCK_HEADER > stream.length) return { why: 'a member runs past the end of the unload' };
|
|
196
|
+
const h = stream.subarray(at, at + BLOCK_HEADER);
|
|
197
|
+
const keyLength = h[9];
|
|
198
|
+
const dataLength = h.readUInt16BE(10);
|
|
199
|
+
at += BLOCK_HEADER;
|
|
200
|
+
if (keyLength === 0 && dataLength === 0) break;
|
|
201
|
+
if (ttr === null) ttr = relativeTrack(extents, tracksPerCylinder, h) * 256 + h[8];
|
|
202
|
+
blocks.push(stream.subarray(at + keyLength, at + keyLength + dataLength));
|
|
203
|
+
at += keyLength + dataLength;
|
|
204
|
+
}
|
|
205
|
+
if (!blocks.length) continue;
|
|
206
|
+
const name = byTtr.get(ttr);
|
|
207
|
+
if (name === undefined) { unmatched++; continue; }
|
|
208
|
+
members.push({ name, bytes: joined(blocks.flatMap((b) => recordsOf(b, recfm, lrecl))) });
|
|
209
|
+
}
|
|
210
|
+
const held = new Set(members.map((m) => m.name));
|
|
211
|
+
return {
|
|
212
|
+
members,
|
|
213
|
+
aliases: entries.filter((e) => e.alias).map((e) => e.name),
|
|
214
|
+
...(unmatched ? { unmatched } : {}),
|
|
215
|
+
...(entries.some((e) => !e.alias && !held.has(e.name)) ? { missing: entries.filter((e) => !e.alias && !held.has(e.name)).map((e) => e.name) } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A cylinder number from CC and HH, whose high 12 bits hold the cylinder's high bits on an extended
|
|
220
|
+
// address volume (DEBSTRHH).
|
|
221
|
+
const cylinder = (cc, hh) => (hh >> 4) * 0x10000 + cc;
|
|
222
|
+
|
|
223
|
+
// The track's number from the start of the data set: the tracks of the extents before extent M, then
|
|
224
|
+
// the cylinders and tracks into M.
|
|
225
|
+
function relativeTrack(extents, tracksPerCylinder, h) {
|
|
226
|
+
const m = h[1];
|
|
227
|
+
const e = extents[m];
|
|
228
|
+
if (!e) return -1;
|
|
229
|
+
const before = extents.slice(0, m).reduce((n, x) => n + x.tracks, 0);
|
|
230
|
+
const hh = h.readUInt16BE(6);
|
|
231
|
+
return before + (cylinder(h.readUInt16BE(4), hh) - e.cyl) * tracksPerCylinder + ((hh & 0x0f) - e.trk);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// A physical block's records: fixed-length records LRECL at a time, variable ones after the BDW each
|
|
235
|
+
// by its RDW, an undefined block as one record. RECFM's first two bits: 10 fixed, 01 variable, 11
|
|
236
|
+
// undefined.
|
|
237
|
+
function recordsOf(block, recfm, lrecl) {
|
|
238
|
+
if (recfm === 2 && lrecl > 0) {
|
|
239
|
+
const out = [];
|
|
240
|
+
for (let i = 0; i + lrecl <= block.length; i += lrecl) out.push(block.subarray(i, i + lrecl));
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
if (recfm === 1) {
|
|
244
|
+
const out = [];
|
|
245
|
+
for (let i = 4; i + 4 <= block.length;) {
|
|
246
|
+
const len = block.readUInt16BE(i);
|
|
247
|
+
if (len < 4) break;
|
|
248
|
+
out.push(block.subarray(i + 4, i + len));
|
|
249
|
+
i += len;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
return [block];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const joined = (records) => Buffer.concat(records.flatMap((r) => [r, Buffer.from([EOR])]));
|
package/lib/kernel/registry.mjs
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
// map it guarded is gone, because a name derived from one place cannot disagree with itself.
|
|
15
15
|
import { scan as scanFlow, scanSteps as scanFlowSteps, RULES as FLOW_RULES } from '../sets/flow.mjs';
|
|
16
16
|
import { scanCics, scanCicsSteps, CICS_RULES } from '../sets/cics.mjs';
|
|
17
|
-
import { scanHidden, HIDDEN_RULES } from '../sets/hidden.mjs';
|
|
17
|
+
import { scanHidden, scanHiddenSteps, HIDDEN_RULES } from '../sets/hidden.mjs';
|
|
18
18
|
import { scanCopybooks, scanCopybooksSteps, COPYBOOK_RULES } from '../sets/copybook.mjs';
|
|
19
19
|
import { scanJcl, JCL_RULES } from '../sets/jcl.mjs';
|
|
20
20
|
import { scanBuild, BUILD_RULES } from '../sets/build.mjs';
|
|
21
|
-
import { scanRecon, RECON_RULES } from '../sets/recon.mjs';
|
|
21
|
+
import { scanRecon, scanReconSteps, RECON_RULES } from '../sets/recon.mjs';
|
|
22
22
|
import { scanVendor, VENDOR_RULES } from '../sets/vendor.mjs';
|
|
23
23
|
import { scanOpaque, OPAQUE_RULES } from '../sets/opaque.mjs';
|
|
24
24
|
import { scanWeb, scanWebSteps, WEB_RULES } from '../sets/web.mjs';
|
|
@@ -29,8 +29,11 @@ import { scanSemantics, scanSemanticsSteps, SEMANTICS_RULES } from '../sets/sema
|
|
|
29
29
|
import { scanZowe, ZOWE_RULES } from '../sets/zowe.mjs';
|
|
30
30
|
import { scanAbend, ABEND_RULES } from '../sets/abend.mjs';
|
|
31
31
|
import { scanHlasm, HLASM_RULES } from '../sets/hlasm.mjs';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
32
|
+
import { scanIms, IMS_RULES } from '../sets/ims.mjs';
|
|
33
|
+
import { scanDb2, DB2_RULES } from '../sets/ddl.mjs';
|
|
34
|
+
import { scanPli, PLI_RULES } from '../sets/pli.mjs';
|
|
35
|
+
import { scanCrypto, scanCryptoSteps, CRYPTO_RULES } from '../sets/crypto.mjs';
|
|
36
|
+
import { scanSecrets, scanSecretsSteps, SECRETS_RULES } from '../sets/secrets.mjs';
|
|
34
37
|
import { toolName } from './ruleset.mjs';
|
|
35
38
|
|
|
36
39
|
// The order is the order a scan runs them and the order --only lists them. It is not significant
|
|
@@ -39,11 +42,11 @@ import { toolName } from './ruleset.mjs';
|
|
|
39
42
|
export const REGISTRY = [
|
|
40
43
|
{ name: 'flow', scan: scanFlow, steps: scanFlowSteps, rules: FLOW_RULES },
|
|
41
44
|
{ name: 'cics', scan: scanCics, steps: scanCicsSteps, rules: CICS_RULES },
|
|
42
|
-
{ name: 'hidden', scan: scanHidden, rules: HIDDEN_RULES },
|
|
45
|
+
{ name: 'hidden', scan: scanHidden, steps: scanHiddenSteps, rules: HIDDEN_RULES },
|
|
43
46
|
{ name: 'copybook', scan: scanCopybooks, steps: scanCopybooksSteps, rules: COPYBOOK_RULES },
|
|
44
47
|
{ name: 'jcl', scan: scanJcl, rules: JCL_RULES },
|
|
45
48
|
{ name: 'build', scan: scanBuild, rules: BUILD_RULES },
|
|
46
|
-
{ name: 'recon', scan: scanRecon, rules: RECON_RULES },
|
|
49
|
+
{ name: 'recon', scan: scanRecon, steps: scanReconSteps, rules: RECON_RULES },
|
|
47
50
|
{ name: 'vendor', scan: scanVendor, rules: VENDOR_RULES },
|
|
48
51
|
{ name: 'opaque', scan: scanOpaque, rules: OPAQUE_RULES },
|
|
49
52
|
{ name: 'web', scan: scanWeb, steps: scanWebSteps, rules: WEB_RULES },
|
|
@@ -54,8 +57,11 @@ export const REGISTRY = [
|
|
|
54
57
|
{ name: 'zowe', scan: scanZowe, rules: ZOWE_RULES },
|
|
55
58
|
{ name: 'abend', scan: scanAbend, rules: ABEND_RULES },
|
|
56
59
|
{ name: 'hlasm', scan: scanHlasm, rules: HLASM_RULES },
|
|
57
|
-
{ name: '
|
|
58
|
-
{ name: '
|
|
60
|
+
{ name: 'ims', scan: scanIms, rules: IMS_RULES },
|
|
61
|
+
{ name: 'ddl', scan: scanDb2, rules: DB2_RULES },
|
|
62
|
+
{ name: 'pli', scan: scanPli, rules: PLI_RULES },
|
|
63
|
+
{ name: 'crypto', scan: scanCrypto, steps: scanCryptoSteps, rules: CRYPTO_RULES },
|
|
64
|
+
{ name: 'secrets', scan: scanSecrets, steps: scanSecretsSteps, rules: SECRETS_RULES },
|
|
59
65
|
];
|
|
60
66
|
|
|
61
67
|
// Re-exported from the kernel's ruleset module, which is where it has to live: this file imports
|
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
import { eachWithinMemory, memoryStatus, stoppedBecause, watchMemoryBuffer, MEMORY_RESERVE } from './memory.mjs';
|
|
15
15
|
|
|
16
16
|
const LOOP = Symbol('loop');
|
|
17
|
+
const LOOPS = Symbol('loops');
|
|
17
18
|
|
|
18
19
|
// What a set yields in place of `eachWithinMemory(items, work, opts)`. `guarded: false` asks for a
|
|
19
20
|
// plain loop: every item, no memory check and no byte budget.
|
|
20
21
|
export const loopOver = (items, work, opts = {}) => ({ [LOOP]: true, items, work, opts });
|
|
21
22
|
|
|
23
|
+
// Several loops of one set walked in the same pass, each with its own account, the set resumed with
|
|
24
|
+
// their accounts in the order given. A file both loops hold goes to the first, then the second.
|
|
25
|
+
export const loopsOver = (...loops) => ({ [LOOPS]: true, loops });
|
|
26
|
+
const loopsOf = (value) => (value[LOOPS] ? value.loops : [value]);
|
|
27
|
+
const resumeWith = (value, runs) => (value[LOOPS] ? runs : runs[0]);
|
|
28
|
+
|
|
22
29
|
function plainLoop(items, work, label) {
|
|
23
30
|
for (let i = 0; i < items.length; i++) work(items[i], i);
|
|
24
31
|
return { label, processed: items.length, bytes: 0, skipped: [], complete: true, stoppedBy: null, peakHeapBytes: 0, note: null };
|
|
@@ -30,24 +37,38 @@ const runAlone = (req) => (req.opts.guarded === false
|
|
|
30
37
|
|
|
31
38
|
export function drive(steps) {
|
|
32
39
|
let step = steps.next();
|
|
33
|
-
while (!step.done) step = steps.next(
|
|
40
|
+
while (!step.done) step = steps.next(resumeWith(step.value, loopsOf(step.value).map(runAlone)));
|
|
34
41
|
return step.value;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
// A tree that, while `focus` names a file, answers text() and parse() for that file once and
|
|
38
|
-
// every later caller the same answer, its error included.
|
|
44
|
+
// A tree that, while `focus` names a file, answers bytes(), text() and parse() for that file once and
|
|
45
|
+
// hands every later caller the same answer, its error included. A tree whose text is its bytes
|
|
46
|
+
// through `decode` has the file read once for both.
|
|
39
47
|
export function sharingTree(base) {
|
|
40
48
|
let focus = null;
|
|
49
|
+
let bytes = null;
|
|
41
50
|
let text = null;
|
|
42
51
|
let parsed = null;
|
|
52
|
+
const once = (held, read) => {
|
|
53
|
+
if (!held) { try { held = { value: read() }; } catch (e) { held = { error: e }; } }
|
|
54
|
+
return held;
|
|
55
|
+
};
|
|
56
|
+
const answer = (held) => {
|
|
57
|
+
if (held.error) throw held.error;
|
|
58
|
+
return held.value;
|
|
59
|
+
};
|
|
43
60
|
const tree = {
|
|
44
61
|
...base,
|
|
45
|
-
focusOn(f) { focus = f; text = null; parsed = null; },
|
|
62
|
+
focusOn(f) { focus = f; bytes = null; text = null; parsed = null; },
|
|
63
|
+
bytes(p) {
|
|
64
|
+
if (p !== focus) return base.bytes(p);
|
|
65
|
+
bytes = once(bytes, () => base.bytes(p));
|
|
66
|
+
return answer(bytes);
|
|
67
|
+
},
|
|
46
68
|
text(p) {
|
|
47
69
|
if (p !== focus) return base.text(p);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return text.value;
|
|
70
|
+
text = once(text, () => (base.decode ? base.decode(tree.bytes(p)) : base.text(p)));
|
|
71
|
+
return answer(text);
|
|
51
72
|
},
|
|
52
73
|
parse(p, src) {
|
|
53
74
|
if (p !== focus) return base.parse(p, src);
|
|
@@ -75,8 +96,12 @@ export function driveTogether(stepsList, tree) {
|
|
|
75
96
|
for (;;) {
|
|
76
97
|
const waiting = live.filter((g) => !g.step.done);
|
|
77
98
|
if (!waiting.length) break;
|
|
78
|
-
const runs = mergedLoop(waiting.
|
|
79
|
-
|
|
99
|
+
const runs = mergedLoop(waiting.flatMap((g) => loopsOf(g.step.value)), tree);
|
|
100
|
+
let at = 0;
|
|
101
|
+
for (const g of waiting) {
|
|
102
|
+
const own = runs.slice(at, at += loopsOf(g.step.value).length);
|
|
103
|
+
advance(g, resumeWith(g.step.value, own.map((r) => r.run)), own.find((r) => r.error)?.error);
|
|
104
|
+
}
|
|
80
105
|
}
|
|
81
106
|
return live.map((g) => (g.error ? { error: g.error } : g.result));
|
|
82
107
|
}
|