forked from mxx1111/sparepack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.mjs
More file actions
140 lines (119 loc) · 4.76 KB
/
Copy pathverify.mjs
File metadata and controls
140 lines (119 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Independent verification of a built pack.
//
// This deliberately re-derives everything from the files on disk instead of trusting the
// manifest. The point is to catch a bug in pack.mjs, and a check that shares its assumptions
// with the thing it checks catches nothing. Notably it re-parses every stripped file to
// confirm no function body survived — if interface stripping ever regresses, this is what
// notices before the pack reaches a stranger.
import { readFile, stat } from 'node:fs/promises'
import { join, relative } from 'node:path'
import { glob } from 'node:fs/promises'
import ts from 'typescript'
import { hasBlockingFindings, scanText } from './scan.mjs'
import { STRIPPED } from './pack.mjs'
const STUB_MARKER = 'sparepack stub: not implemented'
/** A body counts as a stub when it does nothing but throw the marker. */
function isStubBody(body, source) {
if (!body || !ts.isBlock(body)) return false
const statements = body.statements
if (statements.length !== 1) return false
const only = statements[0]
if (!ts.isThrowStatement(only)) return false
return source.slice(only.getStart(), only.getEnd()).includes(STUB_MARKER)
}
/** Walk a stripped file looking for any function body that still contains logic. */
export function findSurvivingBodies(source, path) {
const sf = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
const survivors = []
const visit = (node) => {
const isFunctionLike =
ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node) ||
ts.isConstructorDeclaration(node) ||
ts.isGetAccessorDeclaration(node) ||
ts.isSetAccessorDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node)
if (isFunctionLike && node.body && !isStubBody(node.body, source)) {
const name = node.name?.getText?.() ?? '(anonymous)'
const { line } = sf.getLineAndCharacterOfPosition(node.getStart())
survivors.push({ name, line: line + 1 })
}
ts.forEachChild(node, visit)
}
ts.forEachChild(sf, visit)
return survivors
}
/**
* Verify a pack directory.
* @returns {{ok: boolean, problems: string[], notes: string[], scanned: number}}
*/
export async function verifyPack(dir) {
const problems = []
const notes = []
let manifest
try {
manifest = JSON.parse(await readFile(join(dir, 'MANIFEST.json'), 'utf8'))
} catch (err) {
return {
ok: false,
scanned: 0,
notes,
problems: [
err.code === 'ENOENT'
? `no MANIFEST.json in ${dir} — this does not look like a sparepack`
: `MANIFEST.json is unreadable: ${err.message}`,
],
}
}
const declared = new Map((manifest.files ?? []).map((f) => [f.path, f]))
let scanned = 0
// Every file actually present, not just the ones the manifest admits to.
const present = []
for await (const entry of glob('**/*', { cwd: dir, withFileTypes: true })) {
if (!entry.isFile()) continue
present.push(relative(dir, join(entry.parentPath ?? entry.path, entry.name)))
}
for (const path of present.sort()) {
if (path === 'MANIFEST.json' || path === 'README.md') continue
const entry = declared.get(path)
if (!entry) {
problems.push(`"${path}" is in the pack but not in MANIFEST.json — it was added after packing`)
}
const body = await readFile(join(dir, path), 'utf8')
const bytes = Buffer.byteLength(body)
if (entry && entry.bytes !== bytes) {
problems.push(`"${path}" is ${bytes} bytes but MANIFEST.json says ${entry.bytes} — it changed after packing`)
}
const findings = scanText(body, { path })
scanned++
if (hasBlockingFindings(findings)) {
for (const f of findings.filter((x) => ['critical', 'high'].includes(x.severity))) {
problems.push(`${f.severity} finding in ${f.path}:${f.line} — ${f.label} ${f.excerpt}`)
}
} else if (findings.length) {
notes.push(`${findings.length} low-severity finding(s) in ${path}`)
}
if (entry?.kind === STRIPPED) {
const survivors = findSurvivingBodies(body, path)
for (const s of survivors) {
problems.push(
`"${path}" is marked as stripped but ${s.name} at line ${s.line} still has a real body — ` +
`implementation may have leaked`,
)
}
}
}
for (const path of declared.keys()) {
if (!present.includes(path)) problems.push(`MANIFEST.json lists "${path}" but it is missing from the pack`)
}
if (!(manifest.files ?? []).some((f) => f.role === 'acceptance-test')) {
notes.push('this pack ships no acceptance tests, so "done" is defined in prose only')
}
try {
await stat(join(dir, 'README.md'))
} catch {
notes.push('no README.md — the worker gets no orientation')
}
return { ok: problems.length === 0, problems, notes, scanned }
}