@mpgd/cli 0.13.0 → 0.15.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/dist/index.d.ts +3 -0
- package/dist/index.js +500 -11
- package/dist/microsoft-store-package-acceptance.d.ts +54 -0
- package/dist/microsoft-store-package-acceptance.js +787 -0
- package/dist/microsoft-store-package-generation-contract.d.ts +129 -0
- package/dist/microsoft-store-package-generation-contract.js +3 -0
- package/dist/microsoft-store-package-generation-download.d.ts +22 -0
- package/dist/microsoft-store-package-generation-download.js +453 -0
- package/dist/microsoft-store-package-generation-input.d.ts +3 -0
- package/dist/microsoft-store-package-generation-input.js +384 -0
- package/dist/microsoft-store-package-generation-integrity.d.ts +5 -0
- package/dist/microsoft-store-package-generation-integrity.js +100 -0
- package/dist/microsoft-store-package-generation-manifest.d.ts +3 -0
- package/dist/microsoft-store-package-generation-manifest.js +67 -0
- package/dist/microsoft-store-package-generation-zip.d.ts +1 -0
- package/dist/microsoft-store-package-generation-zip.js +180 -0
- package/dist/microsoft-store-package-generation.d.ts +24 -0
- package/dist/microsoft-store-package-generation.js +430 -0
- package/dist/microsoft-store-starter.d.ts +28 -0
- package/dist/microsoft-store-starter.js +655 -0
- package/dist/microsoft-store-submission.d.ts +79 -0
- package/dist/microsoft-store-submission.js +952 -0
- package/dist/microsoft-store-supported-locales.d.ts +1 -0
- package/dist/microsoft-store-supported-locales.js +33 -0
- package/dist/production-target-readiness.js +3 -1
- package/package.json +9 -8
- package/templates/phaser-game/.agents/skills/release-microsoft-store/SKILL.md +60 -0
- package/templates/phaser-game/.agents/skills/release-microsoft-store/agents/openai.yaml +4 -0
- package/templates/phaser-game/.agents/skills/use-mpgd-kit/SKILL.md +45 -0
- package/templates/phaser-game/.agents/skills/use-mpgd-kit/agents/openai.yaml +4 -0
- package/templates/phaser-game/AGENTS.md +34 -0
- package/templates/phaser-game/README.md +88 -25
- package/templates/phaser-game/agent/acceptance.md +8 -0
- package/templates/phaser-game/agent/brief.md +13 -1
- package/templates/phaser-game/agent/game-manifest.json +7 -0
- package/templates/phaser-game/apps/target-ait/granite.config.ts +35 -0
- package/templates/phaser-game/apps/target-ait/index.html +15 -0
- package/templates/phaser-game/apps/target-ait/package.json +33 -0
- package/templates/phaser-game/apps/target-ait/src/main.ts +34 -0
- package/templates/phaser-game/apps/target-ait/src/styles.css +33 -0
- package/templates/phaser-game/apps/target-ait/tsconfig.json +8 -0
- package/templates/phaser-game/apps/target-ait/vite.config.ts +53 -0
- package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +128 -0
- package/templates/phaser-game/gitignore +7 -0
- package/templates/phaser-game/mpgd.microsoft-store.json +29 -0
- package/templates/phaser-game/mpgd.targets.json +9 -2
- package/templates/phaser-game/package.json +11 -9
- package/templates/phaser-game/vite.shared.ts +13 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { closeSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
|
|
7
|
+
export const microsoftStorePackageAcceptanceSchemaVersion = 1;
|
|
8
|
+
const supportedPackageExtensions = ['.appx', '.appxbundle', '.msix', '.msixbundle'];
|
|
9
|
+
const maximumWindowsPackageCommandDurationMs = 60 * 60 * 1000;
|
|
10
|
+
const maximumWindowsPackageCommandOutputBytes = 16 * 1024 * 1024;
|
|
11
|
+
const maximumWindowsPackageCommandDiagnosticCharacters = 8 * 1024;
|
|
12
|
+
const xmlNamedEntities = {
|
|
13
|
+
quot: '"',
|
|
14
|
+
apos: "'",
|
|
15
|
+
lt: '<',
|
|
16
|
+
gt: '>',
|
|
17
|
+
amp: '&',
|
|
18
|
+
};
|
|
19
|
+
export function createMicrosoftStorePackageAcceptanceRuntime(input = {}) {
|
|
20
|
+
if (process.platform !== 'win32') {
|
|
21
|
+
throw new Error('Microsoft Store package acceptance requires Windows and the Windows SDK.');
|
|
22
|
+
}
|
|
23
|
+
let appCertExecutable;
|
|
24
|
+
if (input.appCertExecutable !== undefined) {
|
|
25
|
+
appCertExecutable = readCanonicalTool(input.appCertExecutable, 'Windows App Certification Kit executable');
|
|
26
|
+
}
|
|
27
|
+
const makeAppxExecutable = input.makeAppxExecutable === undefined
|
|
28
|
+
? findMicrosoftStoreMakeAppxExecutable(path.join(requireEnvironmentPath(process.env['ProgramFiles(x86)'], 'ProgramFiles(x86)'), 'Windows Kits', '10'), process.arch)
|
|
29
|
+
: readCanonicalTool(input.makeAppxExecutable, 'MakeAppx executable');
|
|
30
|
+
return {
|
|
31
|
+
platform: process.platform,
|
|
32
|
+
...(appCertExecutable === undefined ? {} : { appCertExecutable }),
|
|
33
|
+
makeAppxExecutable,
|
|
34
|
+
runCommand: runWindowsCommand,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function findMicrosoftStoreMakeAppxExecutable(windowsKitsDir, architecture) {
|
|
38
|
+
const architectures = windowsSdkArchitectures(architecture);
|
|
39
|
+
const binDir = path.join(windowsKitsDir, 'bin');
|
|
40
|
+
const versionDirectories = readDirectoryNames(binDir)
|
|
41
|
+
.filter((name) => /^\d+(?:\.\d+)+$/u.test(name))
|
|
42
|
+
.sort(compareWindowsSdkVersionsDescending);
|
|
43
|
+
const candidates = versionDirectories.flatMap((version) => architectures.map((candidateArchitecture) => path.join(binDir, version, candidateArchitecture, 'makeappx.exe')));
|
|
44
|
+
candidates.push(path.join(windowsKitsDir, 'App Certification Kit', 'makeappx.exe'));
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
return readCanonicalTool(candidate, 'MakeAppx executable');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!isMissingFileError(error)) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error('MakeAppx executable must exist in a Windows SDK bin directory or the App Certification Kit; pass --makeappx to select it explicitly.');
|
|
56
|
+
}
|
|
57
|
+
export function runMicrosoftStorePackageAcceptance(input, runtime = createMicrosoftStorePackageAcceptanceRuntime()) {
|
|
58
|
+
if (runtime.platform !== 'win32') {
|
|
59
|
+
throw new Error('Microsoft Store package acceptance runtime must use Windows.');
|
|
60
|
+
}
|
|
61
|
+
if (input.packageFiles.length === 0) {
|
|
62
|
+
throw new Error('Microsoft Store package acceptance requires at least one package.');
|
|
63
|
+
}
|
|
64
|
+
const gameRoot = readCanonicalDirectory(input.gameRoot, 'game root');
|
|
65
|
+
const outputDir = readCanonicalDirectoryInside(gameRoot, input.outputDir, 'Microsoft Store package acceptance output directory');
|
|
66
|
+
const submissionEvidenceFile = readCanonicalFileInside(gameRoot, input.submissionEvidenceFile, 'Microsoft Store submission evidence');
|
|
67
|
+
const submissionEvidenceContents = readFileSync(submissionEvidenceFile);
|
|
68
|
+
const submissionEvidence = parseSubmissionEvidence(parseJson(submissionEvidenceContents.toString('utf8'), submissionEvidenceFile, 'Microsoft Store submission evidence'));
|
|
69
|
+
const packageFiles = input.packageFiles.map((file) => readCanonicalPackage(gameRoot, file));
|
|
70
|
+
if (new Set(packageFiles).size !== packageFiles.length) {
|
|
71
|
+
throw new Error('Microsoft Store package acceptance package paths must be unique.');
|
|
72
|
+
}
|
|
73
|
+
const jsonFile = resolveOutputFileInside(gameRoot, path.join(outputDir, 'package-acceptance.json'), 'package acceptance JSON');
|
|
74
|
+
const markdownFile = resolveOutputFileInside(gameRoot, path.join(outputDir, 'package-acceptance.md'), 'package acceptance Markdown');
|
|
75
|
+
const reportFiles = packageFiles.map((packageFile, index) => resolveOutputFileInside(gameRoot, windowsAppCertificationReportFile(outputDir, packageFile, index), `Windows App Certification Kit report ${index + 1}`));
|
|
76
|
+
assertDistinctEvidenceFiles([
|
|
77
|
+
{ file: jsonFile, label: 'package acceptance JSON' },
|
|
78
|
+
{ file: markdownFile, label: 'package acceptance Markdown' },
|
|
79
|
+
...reportFiles.map((file, index) => ({
|
|
80
|
+
file,
|
|
81
|
+
label: `Windows App Certification Kit report ${index + 1}`,
|
|
82
|
+
})),
|
|
83
|
+
], [
|
|
84
|
+
{ file: submissionEvidenceFile, label: 'submission evidence' },
|
|
85
|
+
...packageFiles.map((file) => ({ file, label: 'Microsoft Store package' })),
|
|
86
|
+
]);
|
|
87
|
+
for (const reportFile of reportFiles) {
|
|
88
|
+
rmSync(reportFile, { force: true });
|
|
89
|
+
}
|
|
90
|
+
rmSync(jsonFile, { force: true });
|
|
91
|
+
rmSync(markdownFile, { force: true });
|
|
92
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-microsoft-store-package-'));
|
|
93
|
+
try {
|
|
94
|
+
const packages = packageFiles.map((packageFile, index) => {
|
|
95
|
+
const packageSnapshot = hashFileSnapshot(packageFile);
|
|
96
|
+
const inspected = inspectPackage({
|
|
97
|
+
packageFile,
|
|
98
|
+
tempRoot: path.join(tempRoot, String(index + 1).padStart(2, '0')),
|
|
99
|
+
runtime,
|
|
100
|
+
});
|
|
101
|
+
for (const identity of [inspected.identity, ...inspected.payloadIdentities]) {
|
|
102
|
+
assertExpectedIdentity(identity, submissionEvidence.productIdentity);
|
|
103
|
+
}
|
|
104
|
+
const certification = runOptionalWindowsAppCertification({
|
|
105
|
+
packageFile,
|
|
106
|
+
outputDir,
|
|
107
|
+
packageIndex: index,
|
|
108
|
+
runtime,
|
|
109
|
+
gameRoot,
|
|
110
|
+
reportFile: reportFiles[index],
|
|
111
|
+
});
|
|
112
|
+
const acceptedPackageSnapshot = hashFileSnapshot(packageFile);
|
|
113
|
+
if (acceptedPackageSnapshot.sizeBytes !== packageSnapshot.sizeBytes
|
|
114
|
+
|| acceptedPackageSnapshot.sha256 !== packageSnapshot.sha256) {
|
|
115
|
+
throw new Error(`Microsoft Store package changed during acceptance: ${packageFile}`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
file: relativeOrAbsolute(gameRoot, packageFile),
|
|
119
|
+
sizeBytes: packageSnapshot.sizeBytes,
|
|
120
|
+
sha256: packageSnapshot.sha256,
|
|
121
|
+
identity: inspected.identity,
|
|
122
|
+
payloadIdentities: inspected.payloadIdentities,
|
|
123
|
+
certification,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const evidence = {
|
|
127
|
+
schemaVersion: microsoftStorePackageAcceptanceSchemaVersion,
|
|
128
|
+
target: 'microsoft-store',
|
|
129
|
+
submissionEvidenceFile: relativeOrAbsolute(gameRoot, submissionEvidenceFile),
|
|
130
|
+
submissionEvidenceSha256: hashBuffer(submissionEvidenceContents),
|
|
131
|
+
productIdentity: submissionEvidence.productIdentity,
|
|
132
|
+
packages,
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(jsonFile, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
136
|
+
writeFileSync(markdownFile, renderMicrosoftStorePackageAcceptanceMarkdown(evidence));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
rmSync(jsonFile, { force: true });
|
|
140
|
+
rmSync(markdownFile, { force: true });
|
|
141
|
+
for (const reportFile of reportFiles) {
|
|
142
|
+
rmSync(reportFile, { force: true });
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
return evidence;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function parseMicrosoftStorePackageIdentity(xml) {
|
|
153
|
+
const identityAttributes = findPackageIdentityAttributes(xml);
|
|
154
|
+
if (identityAttributes === undefined) {
|
|
155
|
+
throw new Error('Microsoft Store package manifest is missing Identity.');
|
|
156
|
+
}
|
|
157
|
+
const attributes = parseXmlAttributes(identityAttributes);
|
|
158
|
+
return {
|
|
159
|
+
name: requireXmlAttribute(attributes, 'Name'),
|
|
160
|
+
publisher: requireXmlAttribute(attributes, 'Publisher'),
|
|
161
|
+
version: requireXmlAttribute(attributes, 'Version'),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function runOptionalWindowsAppCertification(input) {
|
|
165
|
+
const appCertExecutable = input.runtime.appCertExecutable;
|
|
166
|
+
if (appCertExecutable === undefined) {
|
|
167
|
+
return { result: 'NOT_RUN' };
|
|
168
|
+
}
|
|
169
|
+
const reportFile = input.reportFile;
|
|
170
|
+
if (reportFile === undefined) {
|
|
171
|
+
throw new Error('Windows App Certification Kit report path was not prepared.');
|
|
172
|
+
}
|
|
173
|
+
rmSync(reportFile, { force: true });
|
|
174
|
+
input.runtime.runCommand(appCertExecutable, ['reset']);
|
|
175
|
+
input.runtime.runCommand(appCertExecutable, [
|
|
176
|
+
'test',
|
|
177
|
+
'-appxpackagepath',
|
|
178
|
+
input.packageFile,
|
|
179
|
+
'-reportoutputpath',
|
|
180
|
+
reportFile,
|
|
181
|
+
]);
|
|
182
|
+
const canonicalReportFile = readCanonicalFileInside(input.outputDir, reportFile, 'Windows App Certification Kit report');
|
|
183
|
+
const reportContents = readFileSync(canonicalReportFile);
|
|
184
|
+
const result = parseWindowsAppCertificationResult(reportContents.toString('utf8'));
|
|
185
|
+
if (result !== 'PASS') {
|
|
186
|
+
throw new Error(`Windows App Certification Kit failed for ${input.packageFile}.`);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
result,
|
|
190
|
+
reportFile: relativeOrAbsolute(input.gameRoot, canonicalReportFile),
|
|
191
|
+
reportSha256: hashBuffer(reportContents),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function windowsAppCertificationReportFile(outputDir, packageFile, packageIndex) {
|
|
195
|
+
return path.join(outputDir, `${String(packageIndex + 1).padStart(2, '0')}-${safeFileNameSegment(path.basename(packageFile))}.wack.xml`);
|
|
196
|
+
}
|
|
197
|
+
export function parseWindowsAppCertificationResult(xml) {
|
|
198
|
+
const reportTag = xml.match(/<REPORT\b([^>]*)>/iu);
|
|
199
|
+
if (reportTag === null) {
|
|
200
|
+
throw new Error('Windows App Certification Kit report is missing REPORT.');
|
|
201
|
+
}
|
|
202
|
+
const result = requireXmlAttribute(parseXmlAttributes(reportTag[1] ?? ''), 'OVERALL_RESULT').toUpperCase();
|
|
203
|
+
if (result !== 'PASS' && result !== 'FAIL') {
|
|
204
|
+
throw new Error(`Unsupported Windows App Certification Kit result: ${result}`);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
export function renderMicrosoftStorePackageAcceptanceMarkdown(evidence) {
|
|
209
|
+
const lines = [
|
|
210
|
+
'# Microsoft Store Package Acceptance',
|
|
211
|
+
'',
|
|
212
|
+
`- Package ID: ${escapeMarkdownInline(evidence.productIdentity.packageId)}`,
|
|
213
|
+
`- Publisher ID: ${escapeMarkdownInline(evidence.productIdentity.publisherId)}`,
|
|
214
|
+
`- Submission evidence SHA-256: ${evidence.submissionEvidenceSha256}`,
|
|
215
|
+
'',
|
|
216
|
+
'| Package | Version | Bytes | SHA-256 | WACK |',
|
|
217
|
+
'| --- | --- | ---: | --- | --- |',
|
|
218
|
+
];
|
|
219
|
+
for (const acceptedPackage of evidence.packages) {
|
|
220
|
+
lines.push(`| ${escapeMarkdownTable(acceptedPackage.file)} | ${escapeMarkdownTable(acceptedPackage.identity.version)} | ${acceptedPackage.sizeBytes} | ${acceptedPackage.sha256} | ${acceptedPackage.certification.result} |`);
|
|
221
|
+
}
|
|
222
|
+
return `${lines.join('\n')}\n`;
|
|
223
|
+
}
|
|
224
|
+
function inspectPackage(input) {
|
|
225
|
+
const extension = path.extname(input.packageFile).toLowerCase();
|
|
226
|
+
mkdirSync(input.tempRoot, { recursive: true });
|
|
227
|
+
if (extension === '.appxbundle' || extension === '.msixbundle') {
|
|
228
|
+
const bundleDir = path.join(input.tempRoot, 'bundle');
|
|
229
|
+
const canonicalTempRoot = readCanonicalDirectory(input.tempRoot, 'Microsoft Store package inspection directory');
|
|
230
|
+
// `/l` preserves links so the fail-closed traversal below can reject them explicitly.
|
|
231
|
+
input.runtime.runCommand(input.runtime.makeAppxExecutable, [
|
|
232
|
+
'unbundle',
|
|
233
|
+
'/p',
|
|
234
|
+
input.packageFile,
|
|
235
|
+
'/d',
|
|
236
|
+
bundleDir,
|
|
237
|
+
'/o',
|
|
238
|
+
'/l',
|
|
239
|
+
]);
|
|
240
|
+
const canonicalBundleDir = readCanonicalDirectory(bundleDir, 'Microsoft Store unpacked bundle directory');
|
|
241
|
+
assertInside(canonicalTempRoot, canonicalBundleDir, 'Microsoft Store unpacked bundle directory');
|
|
242
|
+
const bundleFiles = listFiles(canonicalBundleDir);
|
|
243
|
+
const bundleManifestFiles = bundleFiles.filter((file) => equalsAsciiCaseInsensitive(path.basename(file), 'AppxBundleManifest.xml')
|
|
244
|
+
&& equalsAsciiCaseInsensitive(path.basename(path.dirname(file)), 'AppxMetadata'));
|
|
245
|
+
if (bundleManifestFiles.length !== 1) {
|
|
246
|
+
throw new Error(`Microsoft Store unpacked bundle must contain exactly one AppxBundleManifest.xml; found ${bundleManifestFiles.length}.`);
|
|
247
|
+
}
|
|
248
|
+
const bundleManifestFile = readCanonicalFileInside(canonicalBundleDir, bundleManifestFiles[0] ?? '', 'Microsoft Store bundle manifest');
|
|
249
|
+
const unpackedBundleRoot = readCanonicalDirectory(path.dirname(path.dirname(bundleManifestFile)), 'Microsoft Store unpacked bundle root');
|
|
250
|
+
assertInside(canonicalBundleDir, unpackedBundleRoot, 'Microsoft Store unpacked bundle root');
|
|
251
|
+
const unpackedBundleFiles = listFiles(unpackedBundleRoot);
|
|
252
|
+
const identity = parseMicrosoftStorePackageIdentity(readFileSync(bundleManifestFile, 'utf8'));
|
|
253
|
+
const payloads = unpackedBundleFiles.filter((file) => {
|
|
254
|
+
const payloadExtension = path.extname(file).toLowerCase();
|
|
255
|
+
return payloadExtension === '.appx' || payloadExtension === '.msix';
|
|
256
|
+
});
|
|
257
|
+
if (payloads.length === 0) {
|
|
258
|
+
throw new Error(`Microsoft Store bundle contains no app package payloads: ${input.packageFile}`);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
identity,
|
|
262
|
+
payloadIdentities: payloads.map((payload, index) => inspectSinglePackage(payload, path.join(input.tempRoot, `payload-${index + 1}`), input.runtime)),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
identity: inspectSinglePackage(input.packageFile, path.join(input.tempRoot, 'package'), input.runtime),
|
|
267
|
+
payloadIdentities: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function inspectSinglePackage(packageFile, outputDir, runtime) {
|
|
271
|
+
const canonicalParent = readCanonicalDirectory(path.dirname(outputDir), 'Microsoft Store package inspection directory');
|
|
272
|
+
// `/l` preserves links so the fail-closed traversal below can reject them explicitly.
|
|
273
|
+
runtime.runCommand(runtime.makeAppxExecutable, [
|
|
274
|
+
'unpack',
|
|
275
|
+
'/p',
|
|
276
|
+
packageFile,
|
|
277
|
+
'/d',
|
|
278
|
+
outputDir,
|
|
279
|
+
'/o',
|
|
280
|
+
'/l',
|
|
281
|
+
]);
|
|
282
|
+
const canonicalOutputDir = readCanonicalDirectory(outputDir, 'Microsoft Store unpacked package directory');
|
|
283
|
+
assertInside(canonicalParent, canonicalOutputDir, 'Microsoft Store unpacked package directory');
|
|
284
|
+
// Traverse for listFiles' fail-closed symlink rejection; the file list is intentionally unused.
|
|
285
|
+
listFiles(canonicalOutputDir);
|
|
286
|
+
return parseMicrosoftStorePackageIdentity(readFileSync(readCanonicalFileInside(canonicalOutputDir, path.join(canonicalOutputDir, 'AppxManifest.xml'), 'Microsoft Store app package manifest'), 'utf8'));
|
|
287
|
+
}
|
|
288
|
+
function assertExpectedIdentity(identity, expected) {
|
|
289
|
+
if (!equalsAsciiCaseInsensitive(identity.name, expected.packageId)) {
|
|
290
|
+
throw new Error(`Microsoft Store package identity Name must be ${expected.packageId}; received ${identity.name}.`);
|
|
291
|
+
}
|
|
292
|
+
if (identity.publisher !== expected.publisherId) {
|
|
293
|
+
throw new Error(`Microsoft Store package identity Publisher must be ${expected.publisherId}; received ${identity.publisher}.`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function findPackageIdentityAttributes(xml) {
|
|
297
|
+
const stack = [];
|
|
298
|
+
let identityAttributes;
|
|
299
|
+
let offset = 0;
|
|
300
|
+
while (offset < xml.length) {
|
|
301
|
+
const start = xml.indexOf('<', offset);
|
|
302
|
+
if (start < 0) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (xml.startsWith('<!--', start)) {
|
|
306
|
+
offset = requireXmlTerminator(xml, start + 4, '-->', 'comment');
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (xml.startsWith('<![CDATA[', start)) {
|
|
310
|
+
offset = requireXmlTerminator(xml, start + 9, ']]>', 'CDATA section');
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (xml.startsWith('<?', start)) {
|
|
314
|
+
offset = requireXmlTerminator(xml, start + 2, '?>', 'processing instruction');
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (/^<!DOCTYPE\b/iu.test(xml.slice(start))) {
|
|
318
|
+
throw new Error('Microsoft Store package manifest must not contain a DOCTYPE declaration.');
|
|
319
|
+
}
|
|
320
|
+
if (xml.startsWith('<!', start)) {
|
|
321
|
+
throw new Error('Microsoft Store package manifest contains unsupported XML markup.');
|
|
322
|
+
}
|
|
323
|
+
const end = findXmlTagEnd(xml, start + 1);
|
|
324
|
+
const source = xml.slice(start + 1, end);
|
|
325
|
+
const closing = /^\s*\//u.test(source);
|
|
326
|
+
const tagNamePattern = closing ? /^\s*\/\s*([A-Za-z_][\w:.-]*)/u : /^\s*([A-Za-z_][\w:.-]*)/u;
|
|
327
|
+
const tagName = source.match(tagNamePattern)?.[1];
|
|
328
|
+
if (tagName === undefined) {
|
|
329
|
+
throw new Error('Microsoft Store package manifest contains malformed XML markup.');
|
|
330
|
+
}
|
|
331
|
+
if (closing) {
|
|
332
|
+
const opened = stack.pop();
|
|
333
|
+
if (opened !== tagName) {
|
|
334
|
+
throw new Error('Microsoft Store package manifest contains mismatched XML elements.');
|
|
335
|
+
}
|
|
336
|
+
offset = end + 1;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const parent = stack.at(-1);
|
|
340
|
+
const selfClosing = /\/\s*$/u.test(source);
|
|
341
|
+
if (tagName === 'Identity'
|
|
342
|
+
&& stack.length === 1
|
|
343
|
+
&& (parent === 'Package' || parent === 'Bundle')) {
|
|
344
|
+
if (identityAttributes !== undefined) {
|
|
345
|
+
throw new Error('Microsoft Store package manifest must not contain multiple Identity elements.');
|
|
346
|
+
}
|
|
347
|
+
const nameEnd = source.indexOf(tagName) + tagName.length;
|
|
348
|
+
identityAttributes = source.slice(nameEnd, selfClosing ? source.lastIndexOf('/') : undefined);
|
|
349
|
+
}
|
|
350
|
+
if (!selfClosing) {
|
|
351
|
+
stack.push(tagName);
|
|
352
|
+
}
|
|
353
|
+
offset = end + 1;
|
|
354
|
+
}
|
|
355
|
+
if (stack.length !== 0) {
|
|
356
|
+
throw new Error('Microsoft Store package manifest contains unbalanced XML elements.');
|
|
357
|
+
}
|
|
358
|
+
return identityAttributes;
|
|
359
|
+
}
|
|
360
|
+
function findXmlTagEnd(xml, start) {
|
|
361
|
+
let quote;
|
|
362
|
+
for (let index = start; index < xml.length; index += 1) {
|
|
363
|
+
const character = xml[index];
|
|
364
|
+
if (quote === undefined && (character === '"' || character === "'")) {
|
|
365
|
+
quote = character;
|
|
366
|
+
}
|
|
367
|
+
else if (character === quote) {
|
|
368
|
+
quote = undefined;
|
|
369
|
+
}
|
|
370
|
+
else if (quote === undefined && character === '>') {
|
|
371
|
+
return index;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
throw new Error('Microsoft Store package manifest contains an unterminated XML element.');
|
|
375
|
+
}
|
|
376
|
+
function requireXmlTerminator(xml, start, terminator, label) {
|
|
377
|
+
const end = xml.indexOf(terminator, start);
|
|
378
|
+
if (end < 0) {
|
|
379
|
+
throw new Error(`Microsoft Store package manifest contains an unterminated XML ${label}.`);
|
|
380
|
+
}
|
|
381
|
+
return end + terminator.length;
|
|
382
|
+
}
|
|
383
|
+
function equalsAsciiCaseInsensitive(left, right) {
|
|
384
|
+
if (left.length !== right.length) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
388
|
+
const leftCode = left.charCodeAt(index);
|
|
389
|
+
const rightCode = right.charCodeAt(index);
|
|
390
|
+
const normalizedLeft = leftCode >= 0x41 && leftCode <= 0x5a ? leftCode + 0x20 : leftCode;
|
|
391
|
+
const normalizedRight = rightCode >= 0x41 && rightCode <= 0x5a ? rightCode + 0x20 : rightCode;
|
|
392
|
+
if (normalizedLeft !== normalizedRight) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
function windowsSdkArchitectures(architecture) {
|
|
399
|
+
if (architecture === 'arm64') {
|
|
400
|
+
return ['arm64', 'x64', 'x86'];
|
|
401
|
+
}
|
|
402
|
+
if (architecture === 'x64') {
|
|
403
|
+
return ['x64', 'x86'];
|
|
404
|
+
}
|
|
405
|
+
if (architecture === 'ia32') {
|
|
406
|
+
return ['x86'];
|
|
407
|
+
}
|
|
408
|
+
if (architecture === 'arm') {
|
|
409
|
+
return ['arm'];
|
|
410
|
+
}
|
|
411
|
+
throw new Error(`Unsupported Windows SDK host architecture ${architecture}; pass --makeappx to select a compatible executable explicitly.`);
|
|
412
|
+
}
|
|
413
|
+
function readDirectoryNames(dir) {
|
|
414
|
+
try {
|
|
415
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
416
|
+
.filter((entry) => entry.isDirectory())
|
|
417
|
+
.map((entry) => entry.name);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (isMissingFileError(error)) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`Failed to read Windows SDK bin directory: ${dir}`, { cause: error });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function compareWindowsSdkVersionsDescending(left, right) {
|
|
427
|
+
const leftParts = left.split('.').map(Number);
|
|
428
|
+
const rightParts = right.split('.').map(Number);
|
|
429
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
430
|
+
for (let index = 0; index < length; index += 1) {
|
|
431
|
+
const difference = (rightParts[index] ?? 0) - (leftParts[index] ?? 0);
|
|
432
|
+
if (difference !== 0) {
|
|
433
|
+
return difference;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
function parseSubmissionEvidence(input) {
|
|
439
|
+
const root = requireRecord(input, 'Microsoft Store submission evidence');
|
|
440
|
+
if (root.schemaVersion !== 1 || root.target !== 'microsoft-store') {
|
|
441
|
+
throw new Error('Microsoft Store submission evidence must use schemaVersion 1 and target microsoft-store.');
|
|
442
|
+
}
|
|
443
|
+
const identity = requireRecord(root.productIdentity, 'submission evidence productIdentity');
|
|
444
|
+
return {
|
|
445
|
+
productIdentity: {
|
|
446
|
+
packageId: requireNonEmptyString(identity.packageId, 'submission evidence packageId'),
|
|
447
|
+
publisherId: requireNonEmptyString(identity.publisherId, 'submission evidence publisherId'),
|
|
448
|
+
publisherDisplayName: requireNonEmptyString(identity.publisherDisplayName, 'submission evidence publisherDisplayName'),
|
|
449
|
+
reservedName: requireNonEmptyString(identity.reservedName, 'submission evidence reservedName'),
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function parseXmlAttributes(source) {
|
|
454
|
+
const attributes = new Map();
|
|
455
|
+
const pattern = /([A-Za-z_][\w:.-]*)\s*=\s*(["'])(.*?)\2/gu;
|
|
456
|
+
for (const match of source.matchAll(pattern)) {
|
|
457
|
+
const name = match[1];
|
|
458
|
+
const value = match[3];
|
|
459
|
+
if (name !== undefined && value !== undefined) {
|
|
460
|
+
attributes.set(name.toUpperCase(), decodeXmlAttribute(value));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return attributes;
|
|
464
|
+
}
|
|
465
|
+
function requireXmlAttribute(attributes, name) {
|
|
466
|
+
const value = attributes.get(name.toUpperCase());
|
|
467
|
+
if (value === undefined || value.length === 0) {
|
|
468
|
+
throw new Error(`Microsoft Store XML is missing ${name}.`);
|
|
469
|
+
}
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
function decodeXmlAttribute(value) {
|
|
473
|
+
if (/&(?!(?:#x[0-9A-Fa-f]+|#[0-9]+|quot|apos|lt|gt|amp);)/u.test(value)) {
|
|
474
|
+
throw new Error(`Microsoft Store XML contains an unsupported entity reference: ${value}`);
|
|
475
|
+
}
|
|
476
|
+
return value.replace(/&(?:#x([0-9A-Fa-f]+)|#([0-9]+)|(quot|apos|lt|gt|amp));/gu, (reference, hexadecimal, decimal, named) => {
|
|
477
|
+
if (named !== undefined) {
|
|
478
|
+
return xmlNamedEntities[named] ?? reference;
|
|
479
|
+
}
|
|
480
|
+
const codePoint = Number.parseInt(hexadecimal ?? decimal ?? '', hexadecimal === undefined ? 10 : 16);
|
|
481
|
+
if (!Number.isInteger(codePoint)
|
|
482
|
+
|| codePoint === 0
|
|
483
|
+
|| codePoint > 0x10ffff
|
|
484
|
+
|| (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
|
|
485
|
+
throw new Error(`Microsoft Store XML contains an invalid character reference: ${value}`);
|
|
486
|
+
}
|
|
487
|
+
return String.fromCodePoint(codePoint);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function readCanonicalPackage(gameRoot, file) {
|
|
491
|
+
const packageFile = readCanonicalFileInside(gameRoot, file, 'Microsoft Store package');
|
|
492
|
+
const extension = path.extname(packageFile).toLowerCase();
|
|
493
|
+
if (!supportedPackageExtensions.includes(extension)) {
|
|
494
|
+
throw new Error(`Unsupported Microsoft Store package extension ${extension || '(none)'}; expected ${supportedPackageExtensions.join(', ')}.`);
|
|
495
|
+
}
|
|
496
|
+
return packageFile;
|
|
497
|
+
}
|
|
498
|
+
function readCanonicalTool(file, label) {
|
|
499
|
+
let canonical;
|
|
500
|
+
try {
|
|
501
|
+
canonical = realpathSync(file);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
throw new Error(`${label} must exist: ${file} (${formatError(error)})`, { cause: error });
|
|
505
|
+
}
|
|
506
|
+
if (!lstatSync(canonical).isFile()) {
|
|
507
|
+
throw new Error(`${label} must be a regular file: ${canonical}`);
|
|
508
|
+
}
|
|
509
|
+
return canonical;
|
|
510
|
+
}
|
|
511
|
+
function runWindowsCommand(command, args) {
|
|
512
|
+
process.stderr.write(`Running Microsoft Store package command: ${path.basename(command)}\n`);
|
|
513
|
+
const commandSummary = formatWindowsPackageCommand(command, args);
|
|
514
|
+
const result = spawnSync(command, [...args], {
|
|
515
|
+
encoding: 'utf8',
|
|
516
|
+
killSignal: 'SIGTERM',
|
|
517
|
+
maxBuffer: maximumWindowsPackageCommandOutputBytes,
|
|
518
|
+
timeout: maximumWindowsPackageCommandDurationMs,
|
|
519
|
+
windowsHide: true,
|
|
520
|
+
});
|
|
521
|
+
const stdout = result.stdout ?? '';
|
|
522
|
+
const stderr = result.stderr ?? '';
|
|
523
|
+
const output = [stdout, stderr]
|
|
524
|
+
.filter((value) => value.length > 0)
|
|
525
|
+
.join('\n')
|
|
526
|
+
.trim();
|
|
527
|
+
if (result.error !== undefined) {
|
|
528
|
+
if (isTimeoutError(result.error)) {
|
|
529
|
+
throw new Error(`Windows package command timed out after ${maximumWindowsPackageCommandDurationMs}ms: ${commandSummary}${formatCommandOutput(output)}`);
|
|
530
|
+
}
|
|
531
|
+
if (isMaxBufferError(result.error)) {
|
|
532
|
+
throw new Error(`Windows package command exceeded the ${maximumWindowsPackageCommandOutputBytes}-byte output limit: ${commandSummary}${formatCommandOutput(output)}`);
|
|
533
|
+
}
|
|
534
|
+
throw new Error(`Windows package command could not complete: ${commandSummary} (${formatWindowsPackageCommandError(result.error)})${formatCommandOutput(output)}`);
|
|
535
|
+
}
|
|
536
|
+
if (result.signal !== null) {
|
|
537
|
+
throw new Error(`Windows package command was killed by signal ${result.signal}: ${commandSummary}${formatCommandOutput(output)}`);
|
|
538
|
+
}
|
|
539
|
+
if (result.status !== 0) {
|
|
540
|
+
throw new Error(`Windows package command failed with exit code ${String(result.status)}: ${commandSummary}${formatCommandOutput(output)}`);
|
|
541
|
+
}
|
|
542
|
+
if (stdout.length > 0) {
|
|
543
|
+
process.stdout.write(stdout);
|
|
544
|
+
}
|
|
545
|
+
if (stderr.length > 0) {
|
|
546
|
+
process.stderr.write(stderr);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function formatWindowsPackageCommand(command, args) {
|
|
550
|
+
const executable = path.basename(command);
|
|
551
|
+
const operation = args[0]?.toLowerCase();
|
|
552
|
+
if (operation === 'unpack'
|
|
553
|
+
|| operation === 'unbundle'
|
|
554
|
+
|| operation === 'reset'
|
|
555
|
+
|| operation === 'test') {
|
|
556
|
+
return `${executable} ${operation}`;
|
|
557
|
+
}
|
|
558
|
+
return executable;
|
|
559
|
+
}
|
|
560
|
+
function formatWindowsPackageCommandError(error) {
|
|
561
|
+
if (typeof error === 'object'
|
|
562
|
+
&& error !== null
|
|
563
|
+
&& 'code' in error
|
|
564
|
+
&& typeof error.code === 'string') {
|
|
565
|
+
return `error code ${error.code}`;
|
|
566
|
+
}
|
|
567
|
+
return 'unknown error';
|
|
568
|
+
}
|
|
569
|
+
function listFiles(dir) {
|
|
570
|
+
const files = [];
|
|
571
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
572
|
+
const file = path.join(dir, entry.name);
|
|
573
|
+
if (entry.isSymbolicLink()) {
|
|
574
|
+
throw new Error(`Unpacked package symlink is not allowed: ${file}`);
|
|
575
|
+
}
|
|
576
|
+
else if (entry.isDirectory()) {
|
|
577
|
+
files.push(...listFiles(file));
|
|
578
|
+
}
|
|
579
|
+
else if (entry.isFile()) {
|
|
580
|
+
files.push(file);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return files.sort(compareCodeUnits);
|
|
584
|
+
}
|
|
585
|
+
function parseJson(source, file, label) {
|
|
586
|
+
try {
|
|
587
|
+
return JSON.parse(source);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
throw new Error(`Failed to parse ${label} ${file}: ${formatError(error)}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function hashFileSnapshot(file) {
|
|
594
|
+
const hash = createHash('sha256');
|
|
595
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
596
|
+
const descriptor = openSync(file, 'r');
|
|
597
|
+
try {
|
|
598
|
+
const before = fstatSync(descriptor);
|
|
599
|
+
while (true) {
|
|
600
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, null);
|
|
601
|
+
if (bytesRead === 0) {
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
605
|
+
}
|
|
606
|
+
const after = fstatSync(descriptor);
|
|
607
|
+
if (after.dev !== before.dev
|
|
608
|
+
|| after.ino !== before.ino
|
|
609
|
+
|| after.size !== before.size
|
|
610
|
+
|| after.mtimeMs !== before.mtimeMs
|
|
611
|
+
|| after.ctimeMs !== before.ctimeMs) {
|
|
612
|
+
throw new Error(`File changed while it was being hashed: ${file}`);
|
|
613
|
+
}
|
|
614
|
+
return { sizeBytes: before.size, sha256: hash.digest('hex') };
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
closeSync(descriptor);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function hashBuffer(input) {
|
|
621
|
+
return createHash('sha256').update(input).digest('hex');
|
|
622
|
+
}
|
|
623
|
+
function readCanonicalDirectory(input, label) {
|
|
624
|
+
let canonical;
|
|
625
|
+
try {
|
|
626
|
+
canonical = realpathSync(input);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
throw new Error(`${label} must exist: ${input} (${formatError(error)})`);
|
|
630
|
+
}
|
|
631
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
632
|
+
throw new Error(`${label} must be a directory: ${canonical}`);
|
|
633
|
+
}
|
|
634
|
+
return canonical;
|
|
635
|
+
}
|
|
636
|
+
function readCanonicalDirectoryInside(root, input, label) {
|
|
637
|
+
const canonical = readCanonicalDirectory(input, label);
|
|
638
|
+
assertInside(root, canonical, label);
|
|
639
|
+
return canonical;
|
|
640
|
+
}
|
|
641
|
+
function readCanonicalFileInside(root, input, label) {
|
|
642
|
+
let canonical;
|
|
643
|
+
try {
|
|
644
|
+
canonical = realpathSync(input);
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
throw new Error(`${label} must exist: ${input} (${formatError(error)})`);
|
|
648
|
+
}
|
|
649
|
+
assertInside(root, canonical, label);
|
|
650
|
+
if (!lstatSync(canonical).isFile()) {
|
|
651
|
+
throw new Error(`${label} must be a regular file: ${canonical}`);
|
|
652
|
+
}
|
|
653
|
+
return canonical;
|
|
654
|
+
}
|
|
655
|
+
function resolveOutputFileInside(root, file, label) {
|
|
656
|
+
const parent = readCanonicalDirectory(path.dirname(file), `${label} directory`);
|
|
657
|
+
assertInside(root, parent, label);
|
|
658
|
+
let metadata;
|
|
659
|
+
try {
|
|
660
|
+
metadata = lstatSync(file);
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
if (!isMissingFileError(error)) {
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (metadata !== undefined) {
|
|
668
|
+
if (metadata.isSymbolicLink()) {
|
|
669
|
+
throw new Error(`${label} must not be a symbolic link: ${file}`);
|
|
670
|
+
}
|
|
671
|
+
if (!metadata.isFile()) {
|
|
672
|
+
throw new Error(`${label} must be a regular file when it already exists: ${file}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
let resolved = path.join(parent, path.basename(file));
|
|
676
|
+
if (metadata !== undefined) {
|
|
677
|
+
try {
|
|
678
|
+
resolved = realpathSync(file);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
if (!isMissingFileError(error)) {
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
assertInside(root, resolved, label);
|
|
687
|
+
return resolved;
|
|
688
|
+
}
|
|
689
|
+
function assertDistinctEvidenceFiles(outputs, protectedFiles) {
|
|
690
|
+
for (const [index, output] of outputs.entries()) {
|
|
691
|
+
for (const candidate of [...outputs.slice(index + 1), ...protectedFiles]) {
|
|
692
|
+
if (sameFile(output.file, candidate.file)) {
|
|
693
|
+
throw new Error(`${output.label} must not alias ${candidate.label}: ${output.file}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function sameFile(left, right) {
|
|
699
|
+
if (path.relative(left, right).length === 0) {
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const leftMetadata = statSync(left);
|
|
704
|
+
const rightMetadata = statSync(right);
|
|
705
|
+
// Windows may report ino=0, which means file identity is unavailable rather than equal.
|
|
706
|
+
return leftMetadata.ino !== 0
|
|
707
|
+
&& leftMetadata.dev === rightMetadata.dev
|
|
708
|
+
&& leftMetadata.ino === rightMetadata.ino;
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
if (!isMissingFileError(error)) {
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function isMissingFileError(error) {
|
|
718
|
+
if (typeof error !== 'object' || error === null) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
if ('code' in error && error.code === 'ENOENT') {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
return 'cause' in error && isMissingFileError(error.cause);
|
|
725
|
+
}
|
|
726
|
+
function assertInside(root, candidate, label) {
|
|
727
|
+
const relative = path.relative(root, candidate);
|
|
728
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
729
|
+
throw new Error(`${label} must stay inside its allowed root: ${root}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function requireEnvironmentPath(input, label) {
|
|
733
|
+
if (input === undefined || input.length === 0) {
|
|
734
|
+
throw new Error(`Missing Windows environment path: ${label}`);
|
|
735
|
+
}
|
|
736
|
+
return input;
|
|
737
|
+
}
|
|
738
|
+
function requireRecord(input, label) {
|
|
739
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
740
|
+
throw new Error(`${label} must be an object.`);
|
|
741
|
+
}
|
|
742
|
+
return input;
|
|
743
|
+
}
|
|
744
|
+
function requireNonEmptyString(input, label) {
|
|
745
|
+
if (typeof input !== 'string' || input.length === 0 || input.trim() !== input) {
|
|
746
|
+
throw new Error(`${label} must be a non-empty string without surrounding whitespace.`);
|
|
747
|
+
}
|
|
748
|
+
return input;
|
|
749
|
+
}
|
|
750
|
+
function safeFileNameSegment(value) {
|
|
751
|
+
const stem = value.replace(/[^A-Za-z0-9._-]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
752
|
+
return stem.length === 0 ? 'package' : stem;
|
|
753
|
+
}
|
|
754
|
+
function compareCodeUnits(left, right) {
|
|
755
|
+
if (left < right) {
|
|
756
|
+
return -1;
|
|
757
|
+
}
|
|
758
|
+
if (left > right) {
|
|
759
|
+
return 1;
|
|
760
|
+
}
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
function isTimeoutError(error) {
|
|
764
|
+
return typeof error === 'object'
|
|
765
|
+
&& error !== null
|
|
766
|
+
&& 'code' in error
|
|
767
|
+
&& error.code === 'ETIMEDOUT';
|
|
768
|
+
}
|
|
769
|
+
function isMaxBufferError(error) {
|
|
770
|
+
return typeof error === 'object'
|
|
771
|
+
&& error !== null
|
|
772
|
+
&& 'code' in error
|
|
773
|
+
&& error.code === 'ENOBUFS';
|
|
774
|
+
}
|
|
775
|
+
function formatCommandOutput(output) {
|
|
776
|
+
if (output.length === 0) {
|
|
777
|
+
return '';
|
|
778
|
+
}
|
|
779
|
+
const characters = Array.from(output);
|
|
780
|
+
if (characters.length <= maximumWindowsPackageCommandDiagnosticCharacters) {
|
|
781
|
+
return `\n${output}`;
|
|
782
|
+
}
|
|
783
|
+
const headCharacters = Math.floor(maximumWindowsPackageCommandDiagnosticCharacters / 2);
|
|
784
|
+
const tailCharacters = maximumWindowsPackageCommandDiagnosticCharacters - headCharacters;
|
|
785
|
+
const omittedCharacters = characters.length - maximumWindowsPackageCommandDiagnosticCharacters;
|
|
786
|
+
return `\n${characters.slice(0, headCharacters).join('')}\n... ${omittedCharacters} characters omitted ...\n${characters.slice(-tailCharacters).join('')}`;
|
|
787
|
+
}
|