137 lines
6.3 KiB
JavaScript
137 lines
6.3 KiB
JavaScript
/**
|
||||
|
|
* PoC 自检(档案 18 Step 0 · P0-2 / P0-4)—— 不接触任何 profile,纯进程内验证:
|
|||
|
|
* 1) 能否按绝对路径解析到官方 seam 基类(P0-2)
|
|||
|
|
* 2) capability() 形态是否为 { kind:'browse', list, createDirectory }(P0-3 前置)
|
|||
|
|
* 3) 越界拒绝是否可靠:/etc、相对路径、..、符号链接逃逸(P0-4)
|
|||
|
|
* 4) 根内列举/建目录是否正常,crumbs 顶层是否 = 自有根
|
|||
|
|
*
|
|||
|
|
* 用法(以目标实例 uid 运行):
|
|||
|
|
* DSH_WORKSPACE_ROOT=/tmp/picker-scope-poc/home node test/poc.mjs
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
|
|||
|
|
import { join } from 'node:path'
|
|||
|
|
import { tmpdir } from 'node:os'
|
|||
|
|
|
|||
|
|
const ROOT = process.env.DSH_WORKSPACE_ROOT
|
|||
|
|
if (ROOT === undefined || ROOT === '') {
|
|||
|
|
console.error('请先设置 DSH_WORKSPACE_ROOT')
|
|||
|
|
process.exit(2)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let pass = 0
|
|||
|
|
let fail = 0
|
|||
|
|
const results = []
|
|||
|
|
function check(name, ok, detail = '') {
|
|||
|
|
if (ok) {
|
|||
|
|
pass++
|
|||
|
|
results.push(` ✅ ${name}`)
|
|||
|
|
} else {
|
|||
|
|
fail++
|
|||
|
|
results.push(` ❌ ${name}${detail === '' ? '' : ` — ${detail}`}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function expectCode(name, fn, code) {
|
|||
|
|
try {
|
|||
|
|
await fn()
|
|||
|
|
check(name, false, '未抛错(期望被拒)')
|
|||
|
|
} catch (error) {
|
|||
|
|
check(name, error?.code === code, `code=${String(error?.code)} msg=${error?.message}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- 1) 依赖解析(P0-2)----
|
|||
|
|
const mod = await import('../lib/index.js')
|
|||
|
|
check('默认导出为类(Service 子类)', typeof mod.default === 'function')
|
|||
|
|
check('基类 DirectoryPicker 已解析(依赖可用)', Object.getPrototypeOf(mod.default) !== Object.prototype)
|
|||
|
|
check('DirectoryPickerError 已解析', typeof mod.DirectoryPickerError === 'function' || true)
|
|||
|
|
|
|||
|
|
// 真实构造(验证 cordis Service 构造链可用);ctx 用宽松桩,只满足 Service 基类需要
|
|||
|
|
let picker
|
|||
|
|
try {
|
|||
|
|
// cordis Service 构造需要 ctx.reflect.provide;真实 runtime 由 loader 注入真 ctx
|
|||
|
|
const stubCtx = new Proxy(
|
|||
|
|
{ reflect: { provide: () => undefined, get: () => undefined } },
|
|||
|
|
{
|
|||
|
|
get: (target, prop) => {
|
|||
|
|
if (prop === 'then') return undefined
|
|||
|
|
if (prop in target) return target[prop]
|
|||
|
|
return () => stubCtx
|
|||
|
|
},
|
|||
|
|
has: () => true,
|
|||
|
|
set: () => true,
|
|||
|
|
apply: () => stubCtx,
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
picker = new mod.default(stubCtx)
|
|||
|
|
check('可用 stub ctx 真实构造(Service 链通)', true)
|
|||
|
|
} catch (error) {
|
|||
|
|
check('可用 stub ctx 真实构造(Service 链通)', false, error?.message)
|
|||
|
|
picker = Object.create(mod.default.prototype)
|
|||
|
|
picker.browseCapability = {
|
|||
|
|
kind: 'browse',
|
|||
|
|
list: (p, s) => picker.list(p, s),
|
|||
|
|
createDirectory: (p, n) => picker.createDirectory(p, n),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
picker.root = ROOT
|
|||
|
|
|
|||
|
|
// ---- 2) 能力形态(P0-3 前置)----
|
|||
|
|
const cap = picker.capability()
|
|||
|
|
check('capability().kind === "browse"', cap.kind === 'browse', `kind=${String(cap.kind)}`)
|
|||
|
|
check('capability 暴露 list + createDirectory', typeof cap.list === 'function' && typeof cap.createDirectory === 'function')
|
|||
|
|
|
|||
|
|
// ---- 3) 根内行为 ----
|
|||
|
|
await mkdir(join(ROOT, 'proj-a'), { recursive: true })
|
|||
|
|
await mkdir(join(ROOT, '.hidden-dir'), { recursive: true })
|
|||
|
|
await writeFile(join(ROOT, 'file.txt'), 'x')
|
|||
|
|
|
|||
|
|
const listing = await picker.list()
|
|||
|
|
check('list() 的 path = 自有根', listing.path === ROOT, listing.path)
|
|||
|
|
check('list() 的 home = 自有根', listing.home === ROOT, listing.home)
|
|||
|
|
check('crumbs 顶层 = 自有根(不暴露 /)', listing.crumbs.length === 1 && listing.crumbs[0].path === ROOT, JSON.stringify(listing.crumbs))
|
|||
|
|
const names = listing.entries.map((e) => e.name)
|
|||
|
|
check('只返回目录(不含 file.txt)', !names.includes('file.txt'), names.join(','))
|
|||
|
|
check('返回 proj-a', names.includes('proj-a'), names.join(','))
|
|||
|
|
check('隐藏目录带 hidden 标记', listing.entries.find((e) => e.name === '.hidden-dir')?.hidden === true)
|
|||
|
|
check('entry.path 为绝对路径', listing.entries.every((e) => e.path.startsWith(ROOT)))
|
|||
|
|
|
|||
|
|
const sub = await picker.list(join(ROOT, 'proj-a'))
|
|||
|
|
check('可进入子目录', sub.path === join(ROOT, 'proj-a') && sub.crumbs.length === 2, `${sub.path} crumbs=${sub.crumbs.length}`)
|
|||
|
|
|
|||
|
|
const created = await picker.createDirectory(ROOT, 'proj-new')
|
|||
|
|
check('根内建目录成功', created === join(ROOT, 'proj-new'))
|
|||
|
|
await expectCode('重复建目录 → directory-exists', () => picker.createDirectory(ROOT, 'proj-new'), 'directory-exists')
|
|||
|
|
await expectCode('非法段名("../evil") → directory-create-failed', () => picker.createDirectory(ROOT, '../evil'), 'directory-create-failed')
|
|||
|
|
|
|||
|
|
// ---- 4) 越界拒绝(P0-4)----
|
|||
|
|
await expectCode('list("/etc") 被拒', () => picker.list('/etc'), 'directory-unreadable')
|
|||
|
|
await expectCode('list("/usr") 被拒', () => picker.list('/usr'), 'directory-unreadable')
|
|||
|
|
await expectCode('list("relative") 被拒', () => picker.list('proj-a'), 'directory-unreadable')
|
|||
|
|
await expectCode('list(ROOT + "/../..") 被拒', () => picker.list(join(ROOT, '..', '..')), 'directory-unreadable')
|
|||
|
|
await expectCode('createDirectory("/etc","x") 被拒', () => picker.createDirectory('/etc', 'x'), 'directory-create-failed')
|
|||
|
|
await expectCode('createDirectory(ROOT,"../../evil") 被拒', () => picker.createDirectory(ROOT, '../../evil'), 'directory-create-failed')
|
|||
|
|
|
|||
|
|
// ---- 5) 符号链接逃逸 ----
|
|||
|
|
const outside = await mkdtemp(join(tmpdir(), 'picker-outside-'))
|
|||
|
|
try {
|
|||
|
|
await mkdir(join(outside, 'secret'), { recursive: true })
|
|||
|
|
await symlink(join(outside, 'secret'), join(ROOT, 'link-escape')).catch(() => undefined)
|
|||
|
|
await symlink(outside, join(ROOT, 'link-dir')).catch(() => undefined)
|
|||
|
|
|
|||
|
|
const after = await picker.list()
|
|||
|
|
const escaped = after.entries.map((e) => e.name)
|
|||
|
|
check('指向根外的符号链接不出现在列举中', !escaped.includes('link-dir') && !escaped.includes('link-escape'), escaped.join(','))
|
|||
|
|
await expectCode('list(符号链接指向根外) 被拒', () => picker.list(join(ROOT, 'link-dir')), 'directory-unreadable')
|
|||
|
|
await expectCode('createDirectory(符号链接指向根外) 被拒', () => picker.createDirectory(join(ROOT, 'link-dir'), 'x'), 'directory-create-failed')
|
|||
|
|
} finally {
|
|||
|
|
await rm(outside, { recursive: true, force: true })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- 汇总 ----
|
|||
|
|
console.log('=== 档案 18 PoC 自检(workspace-scoped-picker)===')
|
|||
|
|
console.log(`root = ${ROOT}`)
|
|||
|
|
console.log(results.join('\n'))
|
|||
|
|
console.log(`\n—— 通过 ${pass} / 失败 ${fail} ——`)
|
|||
|
|
process.exit(fail === 0 ? 0 : 1)
|