desktop: escape the attribute values interpolated into the MSIX fragments
before-build.mjs builds the msix-extensions.xml fragment and the hidden CLI <Application> entries from string templates, interpolating the display name and the payload-declared launcher stems raw. A value with `&`, `<` or `"` would corrupt the manifest makeappx reads (an opaque 0x80080204 at best, a differently named alias at worst). Every interpolated attribute now goes through xmlAttribute().
This commit is contained in:
@@ -101,8 +101,8 @@ function writeMsixExtensions() {
|
||||
<uap3:AppExtension
|
||||
Name="com.microsoft.windows.copilotkeyprovider"
|
||||
Id="CopilotKeyProvider"
|
||||
DisplayName="${displayName}"
|
||||
Description="Launch ${displayName} with the Copilot key"
|
||||
DisplayName="${xmlAttribute(displayName)}"
|
||||
Description="Launch ${xmlAttribute(displayName)} with the Copilot key"
|
||||
PublicFolder="Public">
|
||||
<uap3:Properties>
|
||||
<SingleTap>hermes://copilot-key/start?state=Tap</SingleTap>
|
||||
@@ -117,6 +117,15 @@ ${aliases}`
|
||||
fs.writeFileSync(file, copilot)
|
||||
}
|
||||
|
||||
// The manifest fragments above are string templates; a display name or a
|
||||
// payload-declared launcher stem carrying `&`, `<` or `"` would otherwise
|
||||
// corrupt the XML makeappx reads (an opaque 0x80080204 at best, a different
|
||||
// alias at worst).
|
||||
/** @param {string} value */
|
||||
export function xmlAttribute(value) {
|
||||
return String(value).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`)
|
||||
}
|
||||
|
||||
/**
|
||||
* One uap5:Extension carries the aliases declared by the payload.
|
||||
* Exported pure for tests.
|
||||
@@ -128,8 +137,8 @@ export function appExecutionAliasApplications(launchers, identity) {
|
||||
// Give each CLI its own hidden application, with one extension per app.
|
||||
return launchers.map((name, index) => {
|
||||
const executable = ['app', 'resources', 'agent-payload', 'bin', `${name}.exe`].join(String.fromCharCode(92))
|
||||
return `<Application Id="${identity.appNamePascal}Cli${index}" Executable="${executable}" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements DisplayName="${identity.displayName}" Description="${identity.displayName} CLI"
|
||||
return `<Application Id="${xmlAttribute(identity.appNamePascal)}Cli${index}" Executable="${xmlAttribute(executable)}" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements DisplayName="${xmlAttribute(identity.displayName)}" Description="${xmlAttribute(identity.displayName)} CLI"
|
||||
Square150x150Logo="assets\\Square150x150Logo.png" Square44x44Logo="assets\\Square44x44Logo.png"
|
||||
BackgroundColor="transparent" AppListEntry="none" />
|
||||
<Extensions>${appExecutionAliasExtensions([name])}</Extensions>
|
||||
@@ -147,11 +156,11 @@ export function appExecutionAliasExtensions(launchers) {
|
||||
return `<uap5:Extension
|
||||
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
|
||||
Category="windows.appExecutionAlias"
|
||||
Executable="${executable(launchers[0])}"
|
||||
Executable="${xmlAttribute(executable(launchers[0]))}"
|
||||
EntryPoint="Windows.FullTrustApplication">
|
||||
<uap5:AppExecutionAlias>
|
||||
${launchers
|
||||
.map((name) => ` <uap5:ExecutionAlias Alias="${name}.exe" />`)
|
||||
.map((name) => ` <uap5:ExecutionAlias Alias="${xmlAttribute(name)}.exe" />`)
|
||||
.join('\n')}
|
||||
</uap5:AppExecutionAlias>
|
||||
</uap5:Extension>`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'vitest'
|
||||
import { appExecutionAliasExtensions } from './before-build.mjs'
|
||||
import { appExecutionAliasApplications, appExecutionAliasExtensions, xmlAttribute } from './before-build.mjs'
|
||||
|
||||
test('one MSIX extension consumes the launchers declared by the payload', () => {
|
||||
const names = ['custom-cli', 'another-cli']
|
||||
@@ -12,3 +12,12 @@ test('one MSIX extension consumes the launchers declared by the payload', () =>
|
||||
assert.ok(!xml.includes('desktop6:Service'))
|
||||
assert.equal(appExecutionAliasExtensions([]), '')
|
||||
})
|
||||
|
||||
test('manifest fragments escape every interpolated attribute value', () => {
|
||||
const xml = appExecutionAliasApplications(['odd"&<name'], { appNamePascal: 'Hermes', displayName: 'Hermes & "Friends" <beta>' })
|
||||
const values = [...xml.matchAll(/="([^"]*)"/g)].map((m) => m[1].replace(/&#\d+;/g, ''))
|
||||
assert.ok(values.length > 0 && values.every((v) => !/[&<>"']/.test(v)), xml)
|
||||
assert.ok(xml.includes('DisplayName="Hermes & "Friends" <beta>"'))
|
||||
assert.ok(xml.includes('Alias="odd"&<name.exe"'))
|
||||
assert.equal(xmlAttribute('plain-name'), 'plain-name')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user