@geastack/apple 0.2.8
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/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- package/targets/shared/build-audio-selftest.sh +50 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
// This module only inspects files, code-signing metadata, and Git metadata. It
|
|
7
|
+
// deliberately has no facility for executing the selected native artifact.
|
|
8
|
+
export const HARD_MUTE_PROVENANCE_SCHEMA = 'gea.apple.native-hard-mute-provenance/v1'
|
|
9
|
+
|
|
10
|
+
export class ArtifactProvenanceError extends Error {
|
|
11
|
+
constructor(code, detail = '') {
|
|
12
|
+
super(detail ? `${code}: ${detail}` : code)
|
|
13
|
+
this.name = 'ArtifactProvenanceError'
|
|
14
|
+
this.code = code
|
|
15
|
+
this.detail = detail
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fail(code, detail = '') {
|
|
20
|
+
throw new ArtifactProvenanceError(code, detail)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireString(value, field) {
|
|
24
|
+
if (typeof value !== 'string' || value.length === 0) fail('invalid-manifest', `${field} must be a non-empty string`)
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireObject(value, field) {
|
|
29
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
30
|
+
fail('invalid-manifest', `${field} must be an object`)
|
|
31
|
+
}
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function canonicalDirectory(directory, code = 'directory-missing') {
|
|
36
|
+
let canonical
|
|
37
|
+
try {
|
|
38
|
+
canonical = fs.realpathSync(directory)
|
|
39
|
+
} catch {
|
|
40
|
+
fail(code, directory)
|
|
41
|
+
}
|
|
42
|
+
if (!fs.statSync(canonical).isDirectory()) fail(code, directory)
|
|
43
|
+
return canonical
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canonicalFile(file, code = 'file-missing') {
|
|
47
|
+
let canonical
|
|
48
|
+
try {
|
|
49
|
+
canonical = fs.realpathSync(file)
|
|
50
|
+
} catch {
|
|
51
|
+
fail(code, file)
|
|
52
|
+
}
|
|
53
|
+
if (!fs.statSync(canonical).isFile()) fail(code, file)
|
|
54
|
+
return canonical
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sha256File(file) {
|
|
58
|
+
const hash = crypto.createHash('sha256')
|
|
59
|
+
const descriptor = fs.openSync(file, 'r')
|
|
60
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024)
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const read = fs.readSync(descriptor, buffer, 0, buffer.length, null)
|
|
64
|
+
if (read === 0) break
|
|
65
|
+
hash.update(buffer.subarray(0, read))
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
fs.closeSync(descriptor)
|
|
69
|
+
}
|
|
70
|
+
return hash.digest('hex')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function countBufferOccurrences(file, literal) {
|
|
74
|
+
const needle = Buffer.from(literal, 'utf8')
|
|
75
|
+
if (needle.length === 0) fail('invalid-gate-literal')
|
|
76
|
+
const haystack = fs.readFileSync(file)
|
|
77
|
+
let count = 0
|
|
78
|
+
let offset = 0
|
|
79
|
+
while (offset <= haystack.length - needle.length) {
|
|
80
|
+
const found = haystack.indexOf(needle, offset)
|
|
81
|
+
if (found < 0) break
|
|
82
|
+
count += 1
|
|
83
|
+
offset = found + needle.length
|
|
84
|
+
}
|
|
85
|
+
return count
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function countExactCStringOccurrences(file, literal) {
|
|
89
|
+
const expected = Buffer.from(literal, 'utf8')
|
|
90
|
+
const contents = fs.readFileSync(file)
|
|
91
|
+
let count = 0
|
|
92
|
+
let start = 0
|
|
93
|
+
while (start < contents.length) {
|
|
94
|
+
while (start < contents.length && (contents[start] < 0x20 || contents[start] > 0x7e)) start += 1
|
|
95
|
+
let end = start
|
|
96
|
+
while (end < contents.length && contents[end] >= 0x20 && contents[end] <= 0x7e) end += 1
|
|
97
|
+
if (end - start === expected.length && contents.subarray(start, end).equals(expected)) count += 1
|
|
98
|
+
start = end + 1
|
|
99
|
+
}
|
|
100
|
+
return count
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function requireSingleGateLiteral(file, literal, role, exactCString = false) {
|
|
104
|
+
const count = exactCString
|
|
105
|
+
? countExactCStringOccurrences(file, literal)
|
|
106
|
+
: countBufferOccurrences(file, literal)
|
|
107
|
+
if (count !== 1) fail(`${role}-hard-mute-gate-count`, `expected 1, found ${count}: ${file}`)
|
|
108
|
+
return count
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runGit(directory, args, code) {
|
|
112
|
+
const result = spawnSync('git', ['-C', directory, ...args], {
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
+
})
|
|
116
|
+
if (result.status !== 0) fail(code, result.stderr.trim() || `${directory}: git ${args.join(' ')}`)
|
|
117
|
+
return result.stdout.trim()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function repositoryForPath(target) {
|
|
121
|
+
const stat = fs.statSync(target)
|
|
122
|
+
const directory = stat.isDirectory() ? target : path.dirname(target)
|
|
123
|
+
const root = canonicalDirectory(runGit(directory, ['rev-parse', '--show-toplevel'], 'dependency-repository-missing'))
|
|
124
|
+
const revision = runGit(root, ['rev-parse', 'HEAD'], 'dependency-revision-missing')
|
|
125
|
+
return { root, revision }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function trackedFileRecord(file, role) {
|
|
129
|
+
const repository = repositoryForPath(file)
|
|
130
|
+
const relativePath = path.relative(repository.root, file).split(path.sep).join('/')
|
|
131
|
+
if (relativePath.startsWith('../') || path.isAbsolute(relativePath)) {
|
|
132
|
+
fail(`${role}-outside-dependency-repository`, file)
|
|
133
|
+
}
|
|
134
|
+
const committedBlob = runGit(
|
|
135
|
+
repository.root,
|
|
136
|
+
['rev-parse', `${repository.revision}:${relativePath}`],
|
|
137
|
+
`${role}-not-tracked-at-dependency-revision`,
|
|
138
|
+
)
|
|
139
|
+
const workingBlob = runGit(repository.root, ['hash-object', file], `${role}-hash-object-failed`)
|
|
140
|
+
if (workingBlob !== committedBlob) {
|
|
141
|
+
fail(`${role}-does-not-match-dependency-revision`, `${relativePath} at ${repository.revision}`)
|
|
142
|
+
}
|
|
143
|
+
return { ...repository, relativePath, blob: committedBlob }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fileRecord(file) {
|
|
147
|
+
return { path: file, sha256: sha256File(file) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function tokenizeMakeDependencies(text) {
|
|
151
|
+
const collapsed = text.replace(/\\\r?\n/g, ' ')
|
|
152
|
+
let colon = -1
|
|
153
|
+
let escaped = false
|
|
154
|
+
for (let index = 0; index < collapsed.length; index += 1) {
|
|
155
|
+
const character = collapsed[index]
|
|
156
|
+
if (escaped) {
|
|
157
|
+
escaped = false
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (character === '\\') {
|
|
161
|
+
escaped = true
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
if (character === ':') {
|
|
165
|
+
colon = index
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (colon < 0) fail('invalid-depfile', 'missing target separator')
|
|
170
|
+
|
|
171
|
+
const dependencies = []
|
|
172
|
+
let token = ''
|
|
173
|
+
escaped = false
|
|
174
|
+
for (const character of collapsed.slice(colon + 1)) {
|
|
175
|
+
if (escaped) {
|
|
176
|
+
token += character
|
|
177
|
+
escaped = false
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
if (character === '\\') {
|
|
181
|
+
escaped = true
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
if (/\s/.test(character)) {
|
|
185
|
+
if (token.length > 0) {
|
|
186
|
+
dependencies.push(token)
|
|
187
|
+
token = ''
|
|
188
|
+
}
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
token += character
|
|
192
|
+
}
|
|
193
|
+
if (escaped) token += '\\'
|
|
194
|
+
if (token.length > 0) dependencies.push(token)
|
|
195
|
+
return dependencies
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dependencyPaths(depfile, workingDirectory) {
|
|
199
|
+
return tokenizeMakeDependencies(fs.readFileSync(depfile, 'utf8')).map((dependency) =>
|
|
200
|
+
path.normalize(path.isAbsolute(dependency) ? dependency : path.resolve(workingDirectory, dependency)),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function walkDepfiles(directory) {
|
|
205
|
+
const depfiles = []
|
|
206
|
+
const visit = (current) => {
|
|
207
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
208
|
+
const full = path.join(current, entry.name)
|
|
209
|
+
if (entry.isDirectory()) visit(full)
|
|
210
|
+
else if (entry.isFile() && entry.name.endsWith('.o.d')) depfiles.push(full)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
visit(directory)
|
|
214
|
+
return depfiles.sort()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function linkedObjects(linkSignature) {
|
|
218
|
+
const objects = new Set()
|
|
219
|
+
for (const line of fs.readFileSync(linkSignature, 'utf8').split(/\r?\n/)) {
|
|
220
|
+
if (!line.startsWith('object=')) continue
|
|
221
|
+
const candidate = line.slice('object='.length)
|
|
222
|
+
if (candidate.length === 0) continue
|
|
223
|
+
objects.add(path.normalize(path.isAbsolute(candidate) ? candidate : path.resolve(path.dirname(linkSignature), candidate)))
|
|
224
|
+
}
|
|
225
|
+
return objects
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function findSelectedObject({ buildDirectory, source, linkSignature, workingDirectory }) {
|
|
229
|
+
const canonicalSource = canonicalFile(source, 'hard-mute-source-missing')
|
|
230
|
+
const linked = linkedObjects(linkSignature)
|
|
231
|
+
const matches = []
|
|
232
|
+
for (const depfileCandidate of walkDepfiles(buildDirectory)) {
|
|
233
|
+
const dependencies = dependencyPaths(depfileCandidate, workingDirectory)
|
|
234
|
+
let includesSource = false
|
|
235
|
+
for (const dependency of dependencies) {
|
|
236
|
+
try {
|
|
237
|
+
if (fs.realpathSync(dependency) === canonicalSource) {
|
|
238
|
+
includesSource = true
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// An unrelated removed dependency is not the selected source.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!includesSource) continue
|
|
246
|
+
const objectCandidate = path.normalize(depfileCandidate.slice(0, -2))
|
|
247
|
+
if (!linked.has(objectCandidate)) continue
|
|
248
|
+
matches.push({ depfile: depfileCandidate, object: objectCandidate, dependencies })
|
|
249
|
+
}
|
|
250
|
+
if (matches.length === 0) fail('hard-mute-source-not-linked', canonicalSource)
|
|
251
|
+
if (matches.length !== 1) fail('hard-mute-source-linked-by-multiple-objects', matches.map(({ object }) => object).join(', '))
|
|
252
|
+
const match = matches[0]
|
|
253
|
+
const object = canonicalFile(match.object, 'hard-mute-object-missing')
|
|
254
|
+
const depfile = canonicalFile(match.depfile, 'hard-mute-depfile-missing')
|
|
255
|
+
const translationUnitCandidate = match.dependencies.find((dependency) => fs.existsSync(dependency) && fs.statSync(dependency).isFile())
|
|
256
|
+
if (!translationUnitCandidate) fail('translation-unit-missing', depfile)
|
|
257
|
+
const translationUnit = canonicalFile(translationUnitCandidate, 'translation-unit-missing')
|
|
258
|
+
return { object, depfile, translationUnit }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function defaultCodeSignatureVerifier(executable) {
|
|
262
|
+
const result = spawnSync('/usr/bin/codesign', ['--verify', '--strict', executable], {
|
|
263
|
+
encoding: 'utf8',
|
|
264
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
|
+
})
|
|
266
|
+
if (result.status !== 0) fail('invalid-code-signature', result.stderr.trim() || executable)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function dependencyRecords(paths) {
|
|
270
|
+
const byRoot = new Map()
|
|
271
|
+
for (const candidate of paths) {
|
|
272
|
+
const canonical = canonicalDirectory(candidate, 'dependency-root-missing')
|
|
273
|
+
const record = repositoryForPath(canonical)
|
|
274
|
+
byRoot.set(record.root, record)
|
|
275
|
+
}
|
|
276
|
+
return [...byRoot.values()].sort((left, right) => left.root.localeCompare(right.root))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function createHardMuteArtifactManifest(options) {
|
|
280
|
+
const gate = requireString(options.gate, 'gate')
|
|
281
|
+
if (!/^[\x20-\x7e]+$/.test(gate)) fail('invalid-gate-literal', 'gate must contain printable ASCII only')
|
|
282
|
+
const executable = canonicalFile(options.executable, 'executable-missing')
|
|
283
|
+
const source = canonicalFile(options.source, 'hard-mute-source-missing')
|
|
284
|
+
const linkSignature = canonicalFile(options.linkSignature, 'link-signature-missing')
|
|
285
|
+
const buildDirectory = canonicalDirectory(options.buildDirectory ?? path.dirname(linkSignature), 'build-directory-missing')
|
|
286
|
+
const workingDirectory = canonicalDirectory(options.workingDirectory ?? process.cwd(), 'build-working-directory-missing')
|
|
287
|
+
const verifyCodeSignature = options.verifyCodeSignature ?? defaultCodeSignatureVerifier
|
|
288
|
+
|
|
289
|
+
verifyCodeSignature(executable)
|
|
290
|
+
requireSingleGateLiteral(source, gate, 'source')
|
|
291
|
+
const selected = findSelectedObject({ buildDirectory, source, linkSignature, workingDirectory })
|
|
292
|
+
requireSingleGateLiteral(selected.object, gate, 'object', true)
|
|
293
|
+
requireSingleGateLiteral(executable, gate, 'executable', true)
|
|
294
|
+
|
|
295
|
+
const sourceGit = trackedFileRecord(source, 'hard-mute-source')
|
|
296
|
+
const translationUnitGit = trackedFileRecord(selected.translationUnit, 'translation-unit')
|
|
297
|
+
const dependencyRoots = [
|
|
298
|
+
sourceGit.root,
|
|
299
|
+
translationUnitGit.root,
|
|
300
|
+
...(options.dependencyRoots ?? []),
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
schema: HARD_MUTE_PROVENANCE_SCHEMA,
|
|
305
|
+
gate,
|
|
306
|
+
buildWorkingDirectory: workingDirectory,
|
|
307
|
+
hardMuteSource: { ...fileRecord(source), git: sourceGit },
|
|
308
|
+
translationUnit: { ...fileRecord(selected.translationUnit), git: translationUnitGit },
|
|
309
|
+
object: fileRecord(selected.object),
|
|
310
|
+
depfile: fileRecord(selected.depfile),
|
|
311
|
+
linkSignature: fileRecord(linkSignature),
|
|
312
|
+
executable: fileRecord(executable),
|
|
313
|
+
dependencies: dependencyRecords(dependencyRoots),
|
|
314
|
+
codeSignatureVerified: true,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function writeManifestAtomic(manifestPath, manifest) {
|
|
319
|
+
const destination = path.resolve(manifestPath)
|
|
320
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
321
|
+
const temporary = `${destination}.tmp.${process.pid}`
|
|
322
|
+
fs.writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
|
|
323
|
+
try {
|
|
324
|
+
fs.renameSync(temporary, destination)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
fs.rmSync(temporary, { force: true })
|
|
327
|
+
throw error
|
|
328
|
+
}
|
|
329
|
+
return destination
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function assertFileHash(record, role) {
|
|
333
|
+
const value = requireObject(record, role)
|
|
334
|
+
const file = canonicalFile(requireString(value.path, `${role}.path`), `${role}-missing`)
|
|
335
|
+
const expected = requireString(value.sha256, `${role}.sha256`)
|
|
336
|
+
const actual = sha256File(file)
|
|
337
|
+
if (actual !== expected) fail(`${role}-sha256-mismatch`, `${actual} != ${expected}: ${file}`)
|
|
338
|
+
return file
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function assertExpectedPath(actual, expected, role) {
|
|
342
|
+
const canonicalExpected = canonicalFile(expected, `${role}-missing`)
|
|
343
|
+
if (actual !== canonicalExpected) fail(`${role}-path-mismatch`, `${actual} != ${canonicalExpected}`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function verifyGitFileRecord(file, record, role) {
|
|
347
|
+
const git = requireObject(record, `${role}.git`)
|
|
348
|
+
const current = trackedFileRecord(file, role)
|
|
349
|
+
for (const field of ['root', 'revision', 'relativePath', 'blob']) {
|
|
350
|
+
if (current[field] !== requireString(git[field], `${role}.git.${field}`)) {
|
|
351
|
+
fail(`${role}-git-${field}-mismatch`, `${current[field]} != ${git[field]}`)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function verifyHardMuteArtifactManifest(options) {
|
|
357
|
+
const manifestPath = canonicalFile(options.manifestPath, 'manifest-missing')
|
|
358
|
+
let manifest
|
|
359
|
+
try {
|
|
360
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
361
|
+
} catch (error) {
|
|
362
|
+
fail('invalid-manifest', error.message)
|
|
363
|
+
}
|
|
364
|
+
requireObject(manifest, 'manifest')
|
|
365
|
+
if (manifest.schema !== HARD_MUTE_PROVENANCE_SCHEMA) fail('unsupported-manifest-schema', String(manifest.schema))
|
|
366
|
+
const expectedGate = requireString(options.gate, 'gate')
|
|
367
|
+
if (!/^[\x20-\x7e]+$/.test(expectedGate)) fail('invalid-gate-literal', 'gate must contain printable ASCII only')
|
|
368
|
+
if (manifest.gate !== expectedGate) fail('hard-mute-gate-mismatch', `${manifest.gate} != ${expectedGate}`)
|
|
369
|
+
|
|
370
|
+
const source = assertFileHash(manifest.hardMuteSource, 'hard-mute-source')
|
|
371
|
+
const translationUnit = assertFileHash(manifest.translationUnit, 'translation-unit')
|
|
372
|
+
const object = assertFileHash(manifest.object, 'object')
|
|
373
|
+
const depfile = assertFileHash(manifest.depfile, 'depfile')
|
|
374
|
+
const linkSignature = assertFileHash(manifest.linkSignature, 'link-signature')
|
|
375
|
+
const executable = assertFileHash(manifest.executable, 'executable')
|
|
376
|
+
assertExpectedPath(source, options.source, 'hard-mute-source')
|
|
377
|
+
assertExpectedPath(executable, options.executable, 'executable')
|
|
378
|
+
|
|
379
|
+
const workingDirectory = canonicalDirectory(
|
|
380
|
+
requireString(manifest.buildWorkingDirectory, 'buildWorkingDirectory'),
|
|
381
|
+
'build-working-directory-missing',
|
|
382
|
+
)
|
|
383
|
+
const selected = findSelectedObject({
|
|
384
|
+
buildDirectory: path.dirname(linkSignature),
|
|
385
|
+
source,
|
|
386
|
+
linkSignature,
|
|
387
|
+
workingDirectory,
|
|
388
|
+
})
|
|
389
|
+
if (selected.object !== object) fail('linked-object-path-mismatch', `${selected.object} != ${object}`)
|
|
390
|
+
if (selected.depfile !== depfile) fail('depfile-path-mismatch', `${selected.depfile} != ${depfile}`)
|
|
391
|
+
if (selected.translationUnit !== translationUnit) {
|
|
392
|
+
fail('translation-unit-path-mismatch', `${selected.translationUnit} != ${translationUnit}`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
requireSingleGateLiteral(source, expectedGate, 'source')
|
|
396
|
+
requireSingleGateLiteral(object, expectedGate, 'object', true)
|
|
397
|
+
requireSingleGateLiteral(executable, expectedGate, 'executable', true)
|
|
398
|
+
verifyGitFileRecord(source, manifest.hardMuteSource.git, 'hard-mute-source')
|
|
399
|
+
verifyGitFileRecord(translationUnit, manifest.translationUnit.git, 'translation-unit')
|
|
400
|
+
|
|
401
|
+
if (!Array.isArray(manifest.dependencies) || manifest.dependencies.length === 0) {
|
|
402
|
+
fail('invalid-manifest', 'dependencies must be a non-empty array')
|
|
403
|
+
}
|
|
404
|
+
const seenRoots = new Set()
|
|
405
|
+
for (const dependency of manifest.dependencies) {
|
|
406
|
+
requireObject(dependency, 'dependency')
|
|
407
|
+
const root = canonicalDirectory(requireString(dependency.root, 'dependency.root'), 'dependency-root-missing')
|
|
408
|
+
if (seenRoots.has(root)) fail('invalid-manifest', `duplicate dependency root: ${root}`)
|
|
409
|
+
seenRoots.add(root)
|
|
410
|
+
const revision = runGit(root, ['rev-parse', 'HEAD'], 'dependency-revision-missing')
|
|
411
|
+
const expectedRevision = requireString(dependency.revision, 'dependency.revision')
|
|
412
|
+
if (revision !== expectedRevision) {
|
|
413
|
+
fail('dependency-revision-mismatch', `${root}: ${revision} != ${expectedRevision}`)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const verifyCodeSignature = options.verifyCodeSignature ?? defaultCodeSignatureVerifier
|
|
418
|
+
verifyCodeSignature(executable)
|
|
419
|
+
if (manifest.codeSignatureVerified !== true) fail('invalid-manifest', 'codeSignatureVerified must be true')
|
|
420
|
+
return manifest
|
|
421
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { statSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const VALID_KINDS = new Set(['c', 'cxx', 'mm'])
|
|
8
|
+
|
|
9
|
+
function compareText(left, right) {
|
|
10
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseCompileEdges(input) {
|
|
14
|
+
if (input === '') return []
|
|
15
|
+
|
|
16
|
+
const lines = input.endsWith('\n') ? input.slice(0, -1).split('\n') : input.split('\n')
|
|
17
|
+
return lines.map((line, index) => {
|
|
18
|
+
const separator = line.indexOf('\t')
|
|
19
|
+
if (separator <= 0 || separator !== line.lastIndexOf('\t')) {
|
|
20
|
+
throw new Error(`compile edge ${index + 1} must be one kind<TAB>source record`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const kind = line.slice(0, separator)
|
|
24
|
+
const source = line.slice(separator + 1)
|
|
25
|
+
if (!VALID_KINDS.has(kind)) {
|
|
26
|
+
throw new Error(`compile edge ${index + 1} has unsupported kind: ${kind}`)
|
|
27
|
+
}
|
|
28
|
+
if (source === '') {
|
|
29
|
+
throw new Error(`compile edge ${index + 1} has an empty source path`)
|
|
30
|
+
}
|
|
31
|
+
return { kind, source }
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function orderCompileEdges(edges, sizeOf = source => statSync(source).size) {
|
|
36
|
+
return edges
|
|
37
|
+
.map((edge, inputIndex) => ({ ...edge, inputIndex, size: sizeOf(edge.source) }))
|
|
38
|
+
.sort((left, right) =>
|
|
39
|
+
right.size - left.size ||
|
|
40
|
+
compareText(left.source, right.source) ||
|
|
41
|
+
compareText(left.kind, right.kind) ||
|
|
42
|
+
left.inputIndex - right.inputIndex,
|
|
43
|
+
)
|
|
44
|
+
.map(({ kind, source, size }) => ({ kind, source, size }))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatCompileEdges(edges) {
|
|
48
|
+
return edges.map(({ kind, source, size }) => `${kind}\t${size}\t${source}\n`).join('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
52
|
+
if (isMain) {
|
|
53
|
+
let input = ''
|
|
54
|
+
process.stdin.setEncoding('utf8')
|
|
55
|
+
process.stdin.on('data', chunk => { input += chunk })
|
|
56
|
+
process.stdin.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
process.stdout.write(formatCompileEdges(orderCompileEdges(parseCompileEdges(input))))
|
|
59
|
+
} catch (error) {
|
|
60
|
+
process.stderr.write(`order-compile-edges: ${error.message}\n`)
|
|
61
|
+
process.exitCode = 1
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import {
|
|
5
|
+
ArtifactProvenanceError,
|
|
6
|
+
createHardMuteArtifactManifest,
|
|
7
|
+
verifyHardMuteArtifactManifest,
|
|
8
|
+
writeManifestAtomic,
|
|
9
|
+
} from './native-artifact-provenance.mjs'
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
process.stderr.write(
|
|
13
|
+
'usage:\n' +
|
|
14
|
+
' preflight-hard-muted-artifact.mjs record --manifest <file> --executable <file> --link-signature <file> --source <file> --gate <literal> [--build-working-directory <dir>] [--dependency-root <dir> ...]\n' +
|
|
15
|
+
' preflight-hard-muted-artifact.mjs verify --manifest <file> --executable <file> --source <file> --gate <literal>\n',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArguments(argv) {
|
|
20
|
+
const [command, ...rest] = argv
|
|
21
|
+
if (command !== 'record' && command !== 'verify') {
|
|
22
|
+
usage()
|
|
23
|
+
process.exitCode = 64
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
const values = { command, dependencyRoots: [] }
|
|
27
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
28
|
+
const option = rest[index]
|
|
29
|
+
const value = rest[index + 1]
|
|
30
|
+
if (!option.startsWith('--') || value === undefined || value.startsWith('--')) {
|
|
31
|
+
usage()
|
|
32
|
+
process.exitCode = 64
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
index += 1
|
|
36
|
+
switch (option) {
|
|
37
|
+
case '--manifest': values.manifestPath = value; break
|
|
38
|
+
case '--executable': values.executable = value; break
|
|
39
|
+
case '--link-signature': values.linkSignature = value; break
|
|
40
|
+
case '--source': values.source = value; break
|
|
41
|
+
case '--gate': values.gate = value; break
|
|
42
|
+
case '--build-working-directory': values.workingDirectory = value; break
|
|
43
|
+
case '--dependency-root': values.dependencyRoots.push(value); break
|
|
44
|
+
default:
|
|
45
|
+
usage()
|
|
46
|
+
process.exitCode = 64
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const required = command === 'record'
|
|
51
|
+
? ['manifestPath', 'executable', 'linkSignature', 'source', 'gate']
|
|
52
|
+
: ['manifestPath', 'executable', 'source', 'gate']
|
|
53
|
+
if (required.some((field) => !values[field])) {
|
|
54
|
+
usage()
|
|
55
|
+
process.exitCode = 64
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
return values
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function run(argv) {
|
|
62
|
+
const options = parseArguments(argv)
|
|
63
|
+
if (!options) return
|
|
64
|
+
try {
|
|
65
|
+
if (options.command === 'record') {
|
|
66
|
+
const manifest = createHardMuteArtifactManifest(options)
|
|
67
|
+
const manifestPath = writeManifestAtomic(options.manifestPath, manifest)
|
|
68
|
+
process.stdout.write(`PASS artifact-provenance-recorded=${path.resolve(manifestPath)}\n`)
|
|
69
|
+
process.stdout.write(`executable_sha256=${manifest.executable.sha256}\n`)
|
|
70
|
+
process.stdout.write(`object_sha256=${manifest.object.sha256}\n`)
|
|
71
|
+
process.stdout.write(`source_sha256=${manifest.hardMuteSource.sha256}\n`)
|
|
72
|
+
for (const dependency of manifest.dependencies) {
|
|
73
|
+
process.stdout.write(`dependency_revision=${dependency.root}@${dependency.revision}\n`)
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const manifest = verifyHardMuteArtifactManifest(options)
|
|
78
|
+
process.stdout.write('PASS artifact-static-hard-mute-gate=present\n')
|
|
79
|
+
process.stdout.write(`executable_sha256=${manifest.executable.sha256}\n`)
|
|
80
|
+
process.stdout.write(`object_sha256=${manifest.object.sha256}\n`)
|
|
81
|
+
process.stdout.write(`source_sha256=${manifest.hardMuteSource.sha256}\n`)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof ArtifactProvenanceError) {
|
|
84
|
+
process.stderr.write(`FAIL reason=${error.code}${error.detail ? ` detail=${JSON.stringify(error.detail)}` : ''}\n`)
|
|
85
|
+
process.exitCode = 1
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
93
|
+
if (isMain) run(process.argv.slice(2))
|