Skip to content

Commit fc11d12

Browse files
committed
fix(cli): sanitize the installer name by Unicode group, not by escape list
U+2028 and U+2029 are Zl/Zp, so the Cc/Cf filter let them through and a deployment-controlled redirect filename could still forge a status line. Enumerating what to strip had cost a patch per class found — C0 and C1, then the bidi overrides, now the line separators — so this keeps whole groups instead. `C` removes every control, format, surrogate, private-use, and unassigned code point, covering ESC and OSC, the bidi overrides and isolates, zero-width characters, and the BOM; `Z` removes every space, line, and paragraph separator. Separators become a plain space rather than vanishing so a name is not run together at the seam, and runs are collapsed so the result cannot be padded to push text off the line. The regression table now names each class that reached the terminal in an earlier round, so a future bypass says which one came back.
1 parent d351ee3 commit fc11d12

2 files changed

Lines changed: 42 additions & 17 deletions

File tree

packages/sim-setup/src/desktop.test.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,28 @@ describe('sanitizeForTerminal', () => {
160160
expect(sanitizeForTerminal('Sim-1.2.3-universal.dmg')).toBe('Sim-1.2.3-universal.dmg')
161161
})
162162

163-
// Bidi overrides and isolates reorder what the reader sees without emitting a
164-
// single control byte, so a range-based C0/C1 filter lets them straight
165-
// through — `gpj.dmg` can be made to render as `dmg.jpg`.
166-
it('strips bidi controls, not just cursor controls', () => {
167-
expect(sanitizeForTerminal('Sim\u202e gmd.eno\u202c.dmg')).toBe('Sim gmd.eno.dmg')
168-
for (const control of ['\u200e', '\u200f', '\u202a', '\u202d', '\u2066', '\u2069', '\u061c']) {
169-
expect(sanitizeForTerminal(`a${control}b`)).toBe('ab')
170-
}
163+
// Every class that reached the terminal in an earlier round, kept as one
164+
// table so a regression names which one came back. Enumerating escapes cost
165+
// a patch per round, which is why the implementation strips whole Unicode
166+
// groups rather than a list.
167+
it.each([
168+
['bidi override', '\u202e', ''],
169+
['bidi isolate', '\u2066', ''],
170+
['bidi mark', '\u200e', ''],
171+
['arabic letter mark', '\u061c', ''],
172+
['zero-width space', '\u200b', ''],
173+
['byte-order mark', '\ufeff', ''],
174+
['line separator', '\u2028', ' '],
175+
['paragraph separator', '\u2029', ' '],
176+
['no-break space', '\u00a0', ' '],
177+
])('neutralizes a %s', (_name, character, expected) => {
178+
expect(sanitizeForTerminal(`a${character}b`)).toBe(`a${expected}b`)
179+
})
180+
181+
// Separators become a space rather than vanishing, so a name is not silently
182+
// run together at the seam.
183+
it('reduces a name built from several classes at once to printable text', () => {
184+
expect(sanitizeForTerminal('Sim\u001b[2K\u202e\u2028\u00a0X.dmg')).toBe('Sim[2K X.dmg')
171185
})
172186

173187
it('caps a name that would overrun the spinner line', () => {

packages/sim-setup/src/desktop.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,29 @@ function deploymentKey(value: string): string {
4141
const MAX_INSTALLER_NAME = 120
4242

4343
/**
44-
* Strips anything that could move the cursor, repaint or retitle the terminal,
45-
* or reorder what the reader sees.
44+
* Reduces an artifact name to printable text before it reaches a TTY.
4645
*
47-
* The artifact name is read out of a redirect the deployment chose, so it is
48-
* remote input on its way to a TTY, and `decodeURIComponent` turns percent-
49-
* encoded bytes into the real characters. Matched by Unicode class rather than
50-
* by hand-listed ranges: `Cc` covers C0 (including ESC), DEL, and C1, while
51-
* `Cf` covers the bidi overrides and isolates that would otherwise survive and
52-
* let a name render in an order it is not written in.
46+
* The name is read out of a redirect the deployment chose, so it is remote
47+
* input, and `decodeURIComponent` turns percent-encoded bytes into the real
48+
* characters. Enumerating what to strip invited a patch per escape found — C0
49+
* and C1, then the bidi overrides, then the line separators — so this keeps
50+
* whole Unicode groups instead: `C` (Other) removes every control, format,
51+
* surrogate, private-use, and unassigned code point, covering ESC and OSC, the
52+
* bidi overrides and isolates, zero-width characters, and the BOM; `Z`
53+
* (Separator) removes every space, line, and paragraph separator, covering
54+
* U+2028/U+2029 and no-break spaces.
55+
*
56+
* Separators become a plain space rather than vanishing, so a name is not
57+
* silently run together at the seam, and runs are collapsed so the result
58+
* cannot be padded out to push text off the line.
5359
*/
5460
export function sanitizeForTerminal(value: string): string {
55-
return truncate(value.replace(/[\p{Cc}\p{Cf}]/gu, ''), MAX_INSTALLER_NAME)
61+
const printable = value
62+
.replace(/\p{C}/gu, '')
63+
.replace(/\p{Z}/gu, ' ')
64+
.replace(/ {2,}/g, ' ')
65+
.trim()
66+
return truncate(printable, MAX_INSTALLER_NAME)
5667
}
5768

5869
export interface DesktopFlags {

0 commit comments

Comments
 (0)