@napi-rs/cli 3.10.4 → 3.10.6
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/dist/cli.js +1943 -153
- package/dist/index.cjs +1942 -152
- package/dist/index.d.cts +19 -7
- package/dist/index.d.ts +19 -7
- package/dist/index.js +1943 -153
- package/docs/build.md +1 -0
- package/docs/wasi.md +351 -8
- package/package.json +2 -2
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +2629 -86
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/templates.spec.ts +2192 -10
- package/src/api/build.ts +38 -1
- package/src/api/templates/load-wasi-template.ts +1412 -30
- package/src/api/templates/wasi-worker-template.ts +191 -63
- package/src/commands/__tests__/build.spec.ts +6 -0
- package/src/def/build.ts +13 -0
- package/src/utils/__tests__/ohos-selfsign.spec.ts +285 -0
- package/src/utils/__tests__/reconciliation.spec.ts +19 -0
- package/src/utils/config.ts +10 -6
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +5 -4
- package/src/utils/ohos-selfsign.ts +646 -0
|
@@ -2,80 +2,184 @@ export const WASI_WORKER_TEMPLATE = `import fs from 'node:fs'
|
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { parse } from 'node:path'
|
|
4
4
|
import { WASI } from 'node:wasi'
|
|
5
|
-
import { parentPort, Worker, workerData } from 'node:worker_threads'
|
|
5
|
+
import { parentPort, threadId, Worker, workerData } from 'node:worker_threads'
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
// The thread spawn that created this worker has already returned, so the loader
|
|
10
|
+
// thread may be waiting on this thread. A failure before the crash hook below is
|
|
11
|
+
// in place (a missing '@napi-rs/wasm-runtime', say) has to raise the crash flags
|
|
12
|
+
// too, then fail the worker as before.
|
|
13
|
+
let handler
|
|
14
|
+
try {
|
|
15
|
+
const {
|
|
16
|
+
instantiateNapiModuleSync,
|
|
17
|
+
MessageHandler,
|
|
18
|
+
getDefaultContext,
|
|
19
|
+
emnapiAsyncWorkPlugin,
|
|
20
|
+
emnapiTSFNPlugin,
|
|
21
|
+
} = require('@napi-rs/wasm-runtime')
|
|
22
|
+
|
|
23
|
+
if (parentPort) {
|
|
24
|
+
parentPort.on('message', (data) => {
|
|
25
|
+
globalThis.onmessage({ data })
|
|
26
|
+
})
|
|
27
|
+
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
Object.assign(globalThis, {
|
|
30
|
+
self: globalThis,
|
|
31
|
+
require,
|
|
32
|
+
Worker,
|
|
33
|
+
importScripts: function (f) {
|
|
34
|
+
// oxlint-disable-next-line no-eval -- WASI importScripts polyfill
|
|
35
|
+
;(0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f)
|
|
36
|
+
},
|
|
37
|
+
postMessage: function (msg) {
|
|
38
|
+
if (parentPort) {
|
|
39
|
+
parentPort.postMessage(msg)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
20
42
|
})
|
|
21
|
-
}
|
|
22
43
|
|
|
23
|
-
|
|
24
|
-
self: globalThis,
|
|
25
|
-
require,
|
|
26
|
-
Worker,
|
|
27
|
-
importScripts: function (f) {
|
|
28
|
-
// oxlint-disable-next-line no-eval -- WASI importScripts polyfill
|
|
29
|
-
;(0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f)
|
|
30
|
-
},
|
|
31
|
-
postMessage: function (msg) {
|
|
32
|
-
if (parentPort) {
|
|
33
|
-
parentPort.postMessage(msg)
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
})
|
|
44
|
+
const emnapiContext = getDefaultContext()
|
|
37
45
|
|
|
38
|
-
const
|
|
46
|
+
const __cwd = process.cwd()
|
|
47
|
+
const __rootDir =
|
|
48
|
+
(workerData && typeof workerData.rootDir === 'string' && workerData.rootDir) ||
|
|
49
|
+
parse(__cwd).root
|
|
50
|
+
const __hostRoot =
|
|
51
|
+
(workerData && typeof workerData.hostRoot === 'string' && workerData.hostRoot) ||
|
|
52
|
+
(process.platform === 'android' ? __cwd : __rootDir)
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
handler = new MessageHandler({
|
|
55
|
+
onLoad({ wasmModule, wasmMemory }) {
|
|
56
|
+
const wasi = new WASI({
|
|
57
|
+
version: 'preview1',
|
|
58
|
+
env: process.env,
|
|
59
|
+
preopens: {
|
|
60
|
+
[__rootDir]: __hostRoot,
|
|
61
|
+
[__hostRoot]: __hostRoot,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
return instantiateNapiModuleSync(wasmModule, {
|
|
66
|
+
childThread: true,
|
|
67
|
+
wasi,
|
|
68
|
+
context: emnapiContext,
|
|
69
|
+
// The wasm links a "basic" emnapi archive (no C async-work /
|
|
70
|
+
// threadsafe-function implementations), so every thread that
|
|
71
|
+
// instantiates it must provide the JavaScript implementations
|
|
72
|
+
// through the emnapi plugins.
|
|
73
|
+
plugins: [emnapiAsyncWorkPlugin, emnapiTSFNPlugin],
|
|
74
|
+
overwriteImports(importObject) {
|
|
75
|
+
importObject.env = {
|
|
76
|
+
...importObject.env,
|
|
77
|
+
...importObject.napi,
|
|
78
|
+
...importObject.emnapi,
|
|
79
|
+
memory: wasmMemory,
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (workerData && workerData.crashFlag instanceof Int32Array) {
|
|
87
|
+
__raiseWasiThreadCrashFlags(error)
|
|
88
|
+
}
|
|
89
|
+
throw error
|
|
90
|
+
}
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
// Tell the loader thread synchronously that this wasm thread died, so its 'exit'
|
|
93
|
+
// teardown does not re-enter wasm and wait on this thread forever. emnapi calls
|
|
94
|
+
// beforeReportError (which runs emnapi_thread_crashed, so the main thread may
|
|
95
|
+
// throw 'unwind' and start exiting) before the 'error' event is even posted.
|
|
96
|
+
// A loader that predates the flag passes none.
|
|
97
|
+
if (workerData && workerData.crashFlag instanceof Int32Array) {
|
|
98
|
+
const __beforeReportError = handler.beforeReportError
|
|
99
|
+
handler.beforeReportError = function (...args) {
|
|
100
|
+
if (!__raiseWasiThreadCrashFlags(args[0])) {
|
|
101
|
+
// No view (the thread was spawned before the loader read the address,
|
|
102
|
+
// say): the export stores into the same word, but only a worker that
|
|
103
|
+
// loaded has an instance to call it on.
|
|
104
|
+
try {
|
|
105
|
+
const __napiThreadCrashed = this.instance?.exports?.napi_wasm_thread_crashed
|
|
106
|
+
if (typeof __napiThreadCrashed === 'function') {
|
|
107
|
+
__napiThreadCrashed()
|
|
74
108
|
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
}
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
return __beforeReportError.apply(this, args)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The error goes into the shared crash report first. The 'error' event that
|
|
116
|
+
// would carry it is dropped when the loader terminates this worker before the
|
|
117
|
+
// event is sent, so the report is the only copy the loader can count on.
|
|
118
|
+
// Layout: three Int32 words — state (0 empty, 1 writing, 2 written), byte
|
|
119
|
+
// length, threadId — then the UTF-8 JSON of { name, message, stack }. Only the
|
|
120
|
+
// first worker to crash writes it, and it is complete before the flag is raised.
|
|
121
|
+
//
|
|
122
|
+
// Then the loader's flag, then the addon's. The loader thread may already be
|
|
123
|
+
// inside wasm, in a cleanup call that waits on this thread; the addon's flag is
|
|
124
|
+
// what those waits check between short slices. It is a word in the shared wasm
|
|
125
|
+
// memory (the loader passes a view of it, see napi_wasm_thread_crash_flag_address),
|
|
126
|
+
// so no instance is needed: a worker that failed while loading raises it too.
|
|
127
|
+
// Once it is up the waits trap, and the loader, whose flag is already up, turns
|
|
128
|
+
// the throw into its crash rejection. Returns whether the addon's flag was raised.
|
|
129
|
+
function __raiseWasiThreadCrashFlags(error) {
|
|
130
|
+
try {
|
|
131
|
+
__writeCrashReport(workerData.crashReport, error)
|
|
132
|
+
} catch {}
|
|
133
|
+
try {
|
|
134
|
+
Atomics.store(workerData.crashFlag, 0, 1)
|
|
135
|
+
} catch {}
|
|
136
|
+
const addonCrashFlag = workerData.addonCrashFlag
|
|
137
|
+
if (!(addonCrashFlag instanceof Int32Array)) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
Atomics.store(addonCrashFlag, 0, 1)
|
|
142
|
+
return true
|
|
143
|
+
} catch {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function __writeCrashReport(report, error) {
|
|
149
|
+
if (!(report instanceof SharedArrayBuffer) || report.byteLength <= 12) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const header = new Int32Array(report, 0, 3)
|
|
153
|
+
if (Atomics.compareExchange(header, 0, 0, 1) !== 0) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
let length = 0
|
|
157
|
+
try {
|
|
158
|
+
const body = new Uint8Array(report, 12)
|
|
159
|
+
const isObject =
|
|
160
|
+
error !== null && (typeof error === 'object' || typeof error === 'function')
|
|
161
|
+
const name = isObject && typeof error.name === 'string' ? error.name : 'Error'
|
|
162
|
+
const message = isObject && typeof error.message === 'string'
|
|
163
|
+
? error.message
|
|
164
|
+
: String(error)
|
|
165
|
+
const stack = isObject && typeof error.stack === 'string' ? error.stack : undefined
|
|
166
|
+
const encoder = new TextEncoder()
|
|
167
|
+
let bytes = encoder.encode(JSON.stringify({ name, message, stack }))
|
|
168
|
+
if (bytes.length > body.length) {
|
|
169
|
+
bytes = encoder.encode(
|
|
170
|
+
JSON.stringify({ name, message: message.slice(0, body.length >> 3) }),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
if (bytes.length <= body.length) {
|
|
174
|
+
body.set(bytes)
|
|
175
|
+
length = bytes.length
|
|
176
|
+
}
|
|
177
|
+
Atomics.store(header, 2, threadId)
|
|
178
|
+
} finally {
|
|
179
|
+
Atomics.store(header, 1, length)
|
|
180
|
+
Atomics.store(header, 0, 2)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
79
183
|
|
|
80
184
|
globalThis.onmessage = function (e) {
|
|
81
185
|
handler.handle(e)
|
|
@@ -165,7 +269,31 @@ const handler = new MessageHandler({
|
|
|
165
269
|
},${errorHandler ? `\n ${errorHandler}` : ''}
|
|
166
270
|
})
|
|
167
271
|
|
|
272
|
+
// When this wasm thread dies, raise the addon's crash flag before emnapi reports
|
|
273
|
+
// it. The loader thread may be inside wasm, in a cleanup call that waits on this
|
|
274
|
+
// thread; the flag is what those waits check between short slices, and once it
|
|
275
|
+
// is up they trap instead of waiting for good. It is a word in the shared wasm
|
|
276
|
+
// memory, and the loader posts a view of it (see
|
|
277
|
+
// napi_wasm_thread_crash_flag_address) after it instantiated the wasm, or right
|
|
278
|
+
// after it created this worker. No view, no flag: an addon built with an older
|
|
279
|
+
// napi, or a failure before the view arrived.
|
|
280
|
+
let __addonCrashFlag
|
|
281
|
+
const __beforeReportError = handler.beforeReportError
|
|
282
|
+
handler.beforeReportError = function (...args) {
|
|
283
|
+
if (__addonCrashFlag !== undefined) {
|
|
284
|
+
try {
|
|
285
|
+
Atomics.store(__addonCrashFlag, 0, 1)
|
|
286
|
+
} catch {}
|
|
287
|
+
}
|
|
288
|
+
return __beforeReportError.apply(this, args)
|
|
289
|
+
}
|
|
290
|
+
|
|
168
291
|
globalThis.onmessage = function (e) {
|
|
292
|
+
const data = e && e.data
|
|
293
|
+
if (data && data.__napiRsAddonCrashFlag instanceof Int32Array) {
|
|
294
|
+
__addonCrashFlag = data.__napiRsAddonCrashFlag
|
|
295
|
+
return
|
|
296
|
+
}
|
|
169
297
|
handler.handle(e)
|
|
170
298
|
}
|
|
171
299
|
`
|
|
@@ -22,3 +22,9 @@ test('build rejects unsupported formats', (t) => {
|
|
|
22
22
|
message: /Invalid value for --format/,
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
test('build self-signs OpenHarmony artifacts unless --no-ohos-sign', (t) => {
|
|
27
|
+
t.true(createBuildCommand([]).ohosSign)
|
|
28
|
+
t.true(createBuildCommand(['--ohos-sign']).ohosSign)
|
|
29
|
+
t.false(createBuildCommand(['--no-ohos-sign']).ohosSign)
|
|
30
|
+
})
|
package/src/def/build.ts
CHANGED
|
@@ -163,6 +163,11 @@ export abstract class BaseBuildCommand extends Command {
|
|
|
163
163
|
description: 'Do not activate the `default` feature',
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
ohosSign = Option.Boolean('--ohos-sign', true, {
|
|
167
|
+
description:
|
|
168
|
+
'Whether to self-sign the built binary for OpenHarmony targets by injecting a `.codesign` fs-verity section, so the `.so` can be loaded on HarmonyOS devices. Only works with `*-unknown-linux-ohos` targets',
|
|
169
|
+
})
|
|
170
|
+
|
|
166
171
|
/**
|
|
167
172
|
* Return the parsed build options.
|
|
168
173
|
*/
|
|
@@ -201,6 +206,7 @@ export abstract class BaseBuildCommand extends Command {
|
|
|
201
206
|
features: this.features,
|
|
202
207
|
allFeatures: this.allFeatures,
|
|
203
208
|
noDefaultFeatures: this.noDefaultFeatures,
|
|
209
|
+
ohosSign: this.ohosSign,
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
}
|
|
@@ -343,11 +349,18 @@ export interface BuildOptions {
|
|
|
343
349
|
* Do not activate the `default` feature
|
|
344
350
|
*/
|
|
345
351
|
noDefaultFeatures?: boolean
|
|
352
|
+
/**
|
|
353
|
+
* Whether to self-sign the built binary for OpenHarmony targets by injecting a `.codesign` fs-verity section, so the `.so` can be loaded on HarmonyOS devices. Only works with `*-unknown-linux-ohos` targets
|
|
354
|
+
*
|
|
355
|
+
* @default true
|
|
356
|
+
*/
|
|
357
|
+
ohosSign?: boolean
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
export function applyDefaultBuildOptions(options: BuildOptions) {
|
|
349
361
|
return {
|
|
350
362
|
dtsCache: true,
|
|
363
|
+
ohosSign: true,
|
|
351
364
|
...options,
|
|
352
365
|
}
|
|
353
366
|
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { chmod, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import test from 'ava'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
checkSelfsign,
|
|
10
|
+
hasCodesignSection,
|
|
11
|
+
merkleRootHash,
|
|
12
|
+
parseElfHeader,
|
|
13
|
+
selfsignMain,
|
|
14
|
+
signElf,
|
|
15
|
+
signFileAtomic,
|
|
16
|
+
stripCodesign,
|
|
17
|
+
} from '../ohos-selfsign.js'
|
|
18
|
+
|
|
19
|
+
// Builds a deterministic minimal ELF64 shared object: a `.text` section,
|
|
20
|
+
// a `.data` section of `dataSize` bytes and the section name string table.
|
|
21
|
+
// `shentsize` widens every section header entry (ELF64 permits entries
|
|
22
|
+
// larger than 64 bytes) and `trailing` appends that many bytes of junk
|
|
23
|
+
// after the section header table — both are legal upstream edge cases.
|
|
24
|
+
// The golden hashes below were produced by signing this exact fixture with
|
|
25
|
+
// the upstream `selfsign.js` from https://github.com/hqzing/ohos-selfsign.
|
|
26
|
+
function makeElf64(dataSize = 16, shentsize = 64, trailing = 0): Buffer {
|
|
27
|
+
const text = Buffer.alloc(32).fill(0xcc)
|
|
28
|
+
const data = Buffer.alloc(dataSize)
|
|
29
|
+
for (let i = 0; i < dataSize; i++) data[i] = (i * 31 + 7) & 0xff
|
|
30
|
+
const shstr = Buffer.from('\0.text\0.data\0.shstrtab\0', 'latin1')
|
|
31
|
+
// name offsets inside shstr: .text=1, .data=7, .shstrtab=13
|
|
32
|
+
const nsec = 4
|
|
33
|
+
const textOff = 64
|
|
34
|
+
const dataOff = textOff + text.length
|
|
35
|
+
const shstrOff = dataOff + data.length
|
|
36
|
+
const shoff = Math.ceil((shstrOff + shstr.length) / 8) * 8
|
|
37
|
+
const total = shoff + nsec * shentsize + trailing
|
|
38
|
+
|
|
39
|
+
const elf = Buffer.alloc(total)
|
|
40
|
+
// ELF64 header
|
|
41
|
+
elf.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0], 0)
|
|
42
|
+
elf.writeUInt16LE(2, 16) // e_type = ET_EXEC
|
|
43
|
+
elf.writeUInt16LE(0x3e, 18) // e_machine = x86_64
|
|
44
|
+
elf.writeUInt32LE(1, 20) // e_version
|
|
45
|
+
elf.writeBigUInt64LE(0n, 24) // e_entry
|
|
46
|
+
elf.writeBigUInt64LE(0n, 32) // e_phoff
|
|
47
|
+
elf.writeBigUInt64LE(BigInt(shoff), 40) // e_shoff
|
|
48
|
+
elf.writeUInt32LE(0, 48) // e_flags
|
|
49
|
+
elf.writeUInt16LE(64, 52) // e_ehsize
|
|
50
|
+
elf.writeUInt16LE(56, 54) // e_phentsize
|
|
51
|
+
elf.writeUInt16LE(0, 56) // e_phnum
|
|
52
|
+
elf.writeUInt16LE(shentsize, 58) // e_shentsize
|
|
53
|
+
elf.writeUInt16LE(nsec, 60) // e_shnum
|
|
54
|
+
elf.writeUInt16LE(3, 62) // e_shstrndx
|
|
55
|
+
|
|
56
|
+
text.copy(elf, textOff)
|
|
57
|
+
data.copy(elf, dataOff)
|
|
58
|
+
shstr.copy(elf, shstrOff)
|
|
59
|
+
|
|
60
|
+
const sh = (
|
|
61
|
+
i: number,
|
|
62
|
+
name: number,
|
|
63
|
+
type: number,
|
|
64
|
+
flags: number,
|
|
65
|
+
off: number,
|
|
66
|
+
size: number,
|
|
67
|
+
align: number,
|
|
68
|
+
) => {
|
|
69
|
+
const e = shoff + i * shentsize
|
|
70
|
+
elf.writeUInt32LE(name, e)
|
|
71
|
+
elf.writeUInt32LE(type, e + 4)
|
|
72
|
+
elf.writeBigUInt64LE(BigInt(flags), e + 8)
|
|
73
|
+
elf.writeBigUInt64LE(0n, e + 16) // sh_addr
|
|
74
|
+
elf.writeBigUInt64LE(BigInt(off), e + 24)
|
|
75
|
+
elf.writeBigUInt64LE(BigInt(size), e + 32)
|
|
76
|
+
elf.writeBigUInt64LE(BigInt(align), e + 48)
|
|
77
|
+
}
|
|
78
|
+
// sh[0] stays SHT_NULL (all zeros)
|
|
79
|
+
sh(1, 1, 1, 6, textOff, text.length, 16) // .text PROGBITS AX
|
|
80
|
+
sh(2, 7, 1, 3, dataOff, data.length, 8) // .data PROGBITS WA
|
|
81
|
+
sh(3, 13, 3, 0, shstrOff, shstr.length, 1) // .shstrtab STRTAB
|
|
82
|
+
|
|
83
|
+
if (trailing > 0) {
|
|
84
|
+
elf.fill(0xa5, shoff + nsec * shentsize) // recognizable trailing payload
|
|
85
|
+
}
|
|
86
|
+
return elf
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sha256hex = (b: Buffer) => createHash('sha256').update(b).digest('hex')
|
|
90
|
+
|
|
91
|
+
test('signElf output is byte-identical to upstream selfsign.js (small file)', (t) => {
|
|
92
|
+
const signed = signElf(makeElf64(16))
|
|
93
|
+
t.true(hasCodesignSection(signed))
|
|
94
|
+
t.is(
|
|
95
|
+
sha256hex(signed),
|
|
96
|
+
'7913969989b797580aeb3554b78343b1a456845f01c9aedd6f5b9dddeb787c77',
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('signElf output is byte-identical to upstream selfsign.js (multi-level merkle)', (t) => {
|
|
101
|
+
// >128 pages of data forces the merkle loop past one level
|
|
102
|
+
const signed = signElf(makeElf64(600 * 1024))
|
|
103
|
+
t.true(hasCodesignSection(signed))
|
|
104
|
+
t.is(
|
|
105
|
+
sha256hex(signed),
|
|
106
|
+
'b936fed982e6b8d89b9e39c4cfd11258a0522152788ca76a7126f75a3e8d5e3a',
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('signElf output is byte-identical to upstream (intermediate merkle levels)', (t) => {
|
|
111
|
+
// >128*128 pages produces merkle intermediate levels, which upstream
|
|
112
|
+
// stores in the .codesign section after the 296B payload
|
|
113
|
+
const signed = signElf(makeElf64(65 * 1024 * 1024))
|
|
114
|
+
t.true(hasCodesignSection(signed))
|
|
115
|
+
t.is(
|
|
116
|
+
sha256hex(signed),
|
|
117
|
+
'9534aeb8c5e42c5fe78d691ef717aa291179286440ef12df16886f130eb7e7a2',
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('signElf supports section entries larger than 64 bytes', (t) => {
|
|
122
|
+
const signed = signElf(makeElf64(16, 72))
|
|
123
|
+
t.true(hasCodesignSection(signed))
|
|
124
|
+
t.is(
|
|
125
|
+
sha256hex(signed),
|
|
126
|
+
'81b6c9ae7cf071c6ad56e5a6ab7929c35ae46fc3081575503f37bf521e1b842e',
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('signElf preserves trailing data after the section header table', (t) => {
|
|
131
|
+
const signed = signElf(makeElf64(16, 64, 512))
|
|
132
|
+
t.true(hasCodesignSection(signed))
|
|
133
|
+
t.is(
|
|
134
|
+
sha256hex(signed),
|
|
135
|
+
'22dd5f1231f00ef9691fa5bfeaa843c0322879d775f37d834e159de423c2a7c7',
|
|
136
|
+
)
|
|
137
|
+
// the 0xa5 trailing block survives inside the signed output
|
|
138
|
+
const idx = signed.indexOf(Buffer.alloc(512, 0xa5))
|
|
139
|
+
t.true(idx > 0)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('merkleRootHash exposes intermediate levels only for deep trees', (t) => {
|
|
143
|
+
// 128 leaf hashes pack to exactly 4096B, so a <=128-page file has no
|
|
144
|
+
// intermediate levels
|
|
145
|
+
const { mid: shallow } = merkleRootHash(Buffer.alloc(128 * 4096, 0x11), 0, 0)
|
|
146
|
+
t.is(shallow.length, 0)
|
|
147
|
+
|
|
148
|
+
// 128*128+1 pages => the second level packs 129*32B > 4096B, so one
|
|
149
|
+
// intermediate level of 129*32 = 4128B exists
|
|
150
|
+
const deep = merkleRootHash(Buffer.alloc((128 * 128 + 1) * 4096, 0x22), 0, 0)
|
|
151
|
+
t.is(deep.mid.length, 129 * 32)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('checkSelfsign validates a signed artifact', (t) => {
|
|
155
|
+
const signed = signElf(makeElf64(16))
|
|
156
|
+
t.deepEqual(checkSelfsign(signed), { ok: true, reason: null })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('checkSelfsign reports the upstream failure reasons', (t) => {
|
|
160
|
+
t.is(checkSelfsign(Buffer.from('MZ....')).reason, 'not ELF64')
|
|
161
|
+
t.is(checkSelfsign(makeElf64(16)).reason, 'no .codesign section')
|
|
162
|
+
|
|
163
|
+
const signed = signElf(makeElf64(16))
|
|
164
|
+
const tampered = Buffer.from(signed)
|
|
165
|
+
tampered[100] ^= 0xff // flip a .text byte
|
|
166
|
+
t.is(checkSelfsign(tampered).reason, 'merkle root mismatch')
|
|
167
|
+
|
|
168
|
+
// corrupt the descriptor's signSize field: find the 296B payload by its
|
|
169
|
+
// ElfSignInfo header (type=1, length=288), the descriptor starts 8B later
|
|
170
|
+
// and signSize is the descriptor's second u32
|
|
171
|
+
const payloadOff = signed.indexOf(Buffer.from([1, 0, 0, 0, 0x20, 0x01, 0, 0]))
|
|
172
|
+
t.true(payloadOff > 0)
|
|
173
|
+
const badDesc = Buffer.from(signed)
|
|
174
|
+
badDesc.writeUInt32LE(24, payloadOff + 8 + 4)
|
|
175
|
+
t.is(checkSelfsign(badDesc).reason, 'signSize mismatch')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('hasCodesignSection detects signed and unsigned ELFs', (t) => {
|
|
179
|
+
const elf = makeElf64()
|
|
180
|
+
t.false(hasCodesignSection(elf))
|
|
181
|
+
t.true(hasCodesignSection(signElf(elf)))
|
|
182
|
+
t.false(hasCodesignSection(Buffer.from('not an elf')))
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('stripCodesign removes the .codesign section', (t) => {
|
|
186
|
+
const elf = makeElf64()
|
|
187
|
+
const unsigned = stripCodesign(elf)
|
|
188
|
+
t.false(unsigned.removed)
|
|
189
|
+
|
|
190
|
+
const signed = signElf(elf)
|
|
191
|
+
const stripped = stripCodesign(signed)
|
|
192
|
+
t.true(stripped.removed)
|
|
193
|
+
t.false(hasCodesignSection(stripped.out))
|
|
194
|
+
// the stripped ELF still has a valid section header table
|
|
195
|
+
t.notThrows(() => parseElfHeader(stripped.out))
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('stripCodesign is byte-identical to upstream (shentsize 72 + trailing data)', (t) => {
|
|
199
|
+
const signed = signElf(makeElf64(16, 72, 512))
|
|
200
|
+
const stripped = stripCodesign(signed)
|
|
201
|
+
t.true(stripped.removed)
|
|
202
|
+
t.is(
|
|
203
|
+
sha256hex(stripped.out),
|
|
204
|
+
'0161662254321d607bdc487f20ca2c72692dea52bcfa2462a63a4acbe2602421',
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('signElf refuses to re-sign without force', (t) => {
|
|
209
|
+
const signed = signElf(makeElf64())
|
|
210
|
+
t.throws(() => signElf(signed), { message: /already has a \.codesign/ })
|
|
211
|
+
const reSigned = signElf(signed, true)
|
|
212
|
+
t.true(hasCodesignSection(reSigned))
|
|
213
|
+
t.true(checkSelfsign(reSigned).ok)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('signElf rejects non-ELF64 input', (t) => {
|
|
217
|
+
t.throws(() => signElf(Buffer.from('MZ....')), { message: 'not ELF64' })
|
|
218
|
+
// ELF32 is rejected too
|
|
219
|
+
const elf32 = makeElf64()
|
|
220
|
+
elf32[4] = 1
|
|
221
|
+
t.throws(() => signElf(elf32), { message: 'not ELF64' })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('signFileAtomic signs in place and preserves the file mode', async (t) => {
|
|
225
|
+
const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-ohos-selfsign-'))
|
|
226
|
+
try {
|
|
227
|
+
const path = join(dir, 'libfoo.so')
|
|
228
|
+
await writeFile(path, makeElf64())
|
|
229
|
+
// 0o755 takes effect on POSIX; on Windows chmod only maps the
|
|
230
|
+
// read-only bit, so assert the mode survives signing rather than
|
|
231
|
+
// asserting the absolute value.
|
|
232
|
+
await chmod(path, 0o755)
|
|
233
|
+
const { mode: modeBefore } = await stat(path)
|
|
234
|
+
const signed = signElf(makeElf64())
|
|
235
|
+
signFileAtomic(path)
|
|
236
|
+
const { mode } = await stat(path)
|
|
237
|
+
t.is(mode & 0o777, modeBefore & 0o777)
|
|
238
|
+
t.deepEqual(await readFile(path), signed)
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(dir, { recursive: true, force: true })
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('signFileAtomic force mode re-signs an already signed file', async (t) => {
|
|
245
|
+
const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-ohos-selfsign-'))
|
|
246
|
+
try {
|
|
247
|
+
const path = join(dir, 'libfoo.so')
|
|
248
|
+
await writeFile(path, makeElf64())
|
|
249
|
+
signFileAtomic(path)
|
|
250
|
+
t.true(checkSelfsign(await readFile(path)).ok)
|
|
251
|
+
// a second pass without force throws; force re-signs cleanly
|
|
252
|
+
t.throws(() => signFileAtomic(path), {
|
|
253
|
+
message: /already has a \.codesign/,
|
|
254
|
+
})
|
|
255
|
+
signFileAtomic(path, true)
|
|
256
|
+
t.true(checkSelfsign(await readFile(path)).ok)
|
|
257
|
+
} finally {
|
|
258
|
+
await rm(dir, { recursive: true, force: true })
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('selfsignMain signs, strips, checks and reports exit codes', async (t) => {
|
|
263
|
+
const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-ohos-selfsign-'))
|
|
264
|
+
try {
|
|
265
|
+
const path = join(dir, 'libfoo.so')
|
|
266
|
+
await writeFile(path, makeElf64())
|
|
267
|
+
// --check on an unsigned file fails
|
|
268
|
+
t.is(selfsignMain(['--check', path]), 1)
|
|
269
|
+
t.is(selfsignMain([path]), 0)
|
|
270
|
+
t.true(hasCodesignSection(await readFile(path)))
|
|
271
|
+
// --check on the signed file passes
|
|
272
|
+
t.is(selfsignMain(['--check', path]), 0)
|
|
273
|
+
// --check combined with --force/--strip/an output path is a usage error
|
|
274
|
+
t.is(selfsignMain(['--check', '--force', path]), 1)
|
|
275
|
+
// re-signing without --force errors with exit code 2
|
|
276
|
+
t.is(selfsignMain([path]), 2)
|
|
277
|
+
t.is(selfsignMain(['--force', path]), 0)
|
|
278
|
+
t.is(selfsignMain(['--strip', path]), 0)
|
|
279
|
+
t.false(hasCodesignSection(await readFile(path)))
|
|
280
|
+
// usage error
|
|
281
|
+
t.is(selfsignMain([]), 1)
|
|
282
|
+
} finally {
|
|
283
|
+
await rm(dir, { recursive: true, force: true })
|
|
284
|
+
}
|
|
285
|
+
})
|
|
@@ -19,6 +19,7 @@ import ava, { type TestFn } from 'ava'
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
createProcessIncarnationGetter,
|
|
22
|
+
getCurrentProcessExecutionIdentity,
|
|
22
23
|
type ProcessExecutionIdentity,
|
|
23
24
|
resolveProcessExecutionIdentityForLocking,
|
|
24
25
|
withFileSystemReconciliation,
|
|
@@ -439,6 +440,24 @@ test('degraded acquisition respects an existing lock it cannot evaluate', async
|
|
|
439
440
|
})
|
|
440
441
|
|
|
441
442
|
test('degraded acquisition heals when identity probes recover and reclaims a stale lock', async (t) => {
|
|
443
|
+
// Phase 1 below relies on the real host identity; a host whose probes fail
|
|
444
|
+
// (e.g. a CI runner where powershell's Get-CimInstance boot probe times
|
|
445
|
+
// out) degrades to lock-free operation by design, writes no owner, and can
|
|
446
|
+
// prove nothing here. Give the guard the same 10s retry budget a normal
|
|
447
|
+
// acquisition uses, so only a host whose probes stay unavailable is
|
|
448
|
+
// skipped; then report it and pass instead of failing on the host.
|
|
449
|
+
const hostIdentity = await resolveProcessExecutionIdentityForLocking(
|
|
450
|
+
getCurrentProcessExecutionIdentity,
|
|
451
|
+
{ expiresAt: performance.now() + 10_000, timeout: 10_000 },
|
|
452
|
+
)
|
|
453
|
+
if (!hostIdentity.complete) {
|
|
454
|
+
t.log(
|
|
455
|
+
`skipping: host identity incomplete (${JSON.stringify(hostIdentity.identity)})`,
|
|
456
|
+
)
|
|
457
|
+
t.pass()
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
442
461
|
const anchor = join(t.context.tmpDir, 'pkg')
|
|
443
462
|
await mkdir(anchor)
|
|
444
463
|
|
package/src/utils/config.ts
CHANGED
|
@@ -144,12 +144,16 @@ export interface UserNapiConfig {
|
|
|
144
144
|
* Whether the generated WASI loaders bootstrap the `napi-async-runtime`
|
|
145
145
|
* CurrentThread JavaScript hosts.
|
|
146
146
|
*
|
|
147
|
-
* A binding built with the `napi-async-runtime` crate
|
|
148
|
-
* `CurrentThread` flavor on every WebAssembly target
|
|
149
|
-
* flavor
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
147
|
+
* A binding built with the `napi-async-runtime` crate defaults to the
|
|
148
|
+
* `CurrentThread` flavor on every WebAssembly target. It is the only
|
|
149
|
+
* flavor on threadless `wasm32-wasip1`; `wasm32-wasip1-threads` also
|
|
150
|
+
* accepts an explicitly configured `MultiThread`. Under `CurrentThread`
|
|
151
|
+
* its futures make no progress until a task host is registered, and its
|
|
152
|
+
* timers never fire until a timer host is, so the loader must install
|
|
153
|
+
* both right after instantiation and unregister them before the
|
|
154
|
+
* environment is destroyed. A `MultiThread` binding uses neither host and
|
|
155
|
+
* the installed task host is unref'd, so leaving this enabled is also
|
|
156
|
+
* correct for an addon that picks its flavor at runtime.
|
|
153
157
|
*
|
|
154
158
|
* Enabling this emits that bootstrap in the node, browser and deferred
|
|
155
159
|
* (workerd) loaders, and declares `@napi-rs/async-runtime` in the
|
package/src/utils/index.ts
CHANGED