From 155149189f09a2a7b5e53a0e38915cf10f6492d1 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 26 Sep 2026 01:19:33 -0700 Subject: [PATCH] fix: quote media scan destination paths --- lib/commands/media.ts | 2 +- test/unit/media.spec.ts | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 test/unit/media.spec.ts diff --git a/lib/commands/media.ts b/lib/commands/media.ts index aa8793a0..38cd2136 100644 --- a/lib/commands/media.ts +++ b/lib/commands/media.ts @@ -13,7 +13,7 @@ import {LOG_PREFIX} from '../logger.js'; export async function scanMedia(this: SettingsApp, destination: string): Promise { this.log.debug(LOG_PREFIX, `Scanning '${destination}' for media files`); await this.checkBroadcast( - ['-n', MEDIA_SCAN_RECEIVER, '-a', MEDIA_SCAN_ACTION, '--es', 'path', destination], + ['-n', MEDIA_SCAN_RECEIVER, '-a', MEDIA_SCAN_ACTION, '--es', 'path', `'${destination.replace(/'/g, `'"'"'`)}'`], 'scan media', ); } diff --git a/test/unit/media.spec.ts b/test/unit/media.spec.ts new file mode 100644 index 00000000..f0a528f5 --- /dev/null +++ b/test/unit/media.spec.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import {execFileSync} from 'node:child_process'; +import {afterEach, beforeEach, describe, it} from 'node:test'; + +import {ADB} from 'appium-adb'; +import sinon from 'sinon'; + +import {SettingsApp} from '../../lib/client.js'; +import {MEDIA_SCAN_ACTION, MEDIA_SCAN_RECEIVER} from '../../lib/constants.js'; + +describe('scanMedia', function () { + const sandbox = sinon.createSandbox(); + let client: SettingsApp; + beforeEach(function () { + client = new SettingsApp({adb: new ADB()}); + }); + afterEach(function () { + sandbox.restore(); + }); + + it('quotes the complete path in the broadcast extra', async function () { + const broadcast = sandbox.stub(client, 'checkBroadcast').resolves(''); + await client.scanMedia("/sdcard/O'Brien!.png"); + sinon.assert.calledOnceWithExactly( + broadcast, + ['-n', MEDIA_SCAN_RECEIVER, '-a', MEDIA_SCAN_ACTION, '--es', 'path', `'/sdcard/O'"'"'Brien!.png'`], + 'scan media', + ); + }); + + it('preserves special characters through the device shell', {skip: process.platform === 'win32'}, async function () { + const values = ['', '/sdcard/two words.png', '/sdcard/O\'Brien! "$HOME" `printf unexpected`;\n[*].png']; + const broadcast = sandbox.stub(client, 'checkBroadcast').resolves(''); + for (const destination of values) { + await client.scanMedia(destination); + const args = broadcast.lastCall.args[0]; + const output = execFileSync('/bin/sh', ['-c', `printf '%s\\0' ${args.join(' ')}`]); + assert.deepStrictEqual(output.toString().split('\0').slice(0, -1), [ + '-n', + MEDIA_SCAN_RECEIVER, + '-a', + MEDIA_SCAN_ACTION, + '--es', + 'path', + destination, + ]); + } + }); +});