diff --git a/lib/units/websocket/index.ts b/lib/units/websocket/index.ts index 93118d2d81..946afe2896 100644 --- a/lib/units/websocket/index.ts +++ b/lib/units/websocket/index.ts @@ -25,6 +25,7 @@ import {TransactionManager} from '../../wire/transmanager.js' import {ClientDispatcher} from './support/clientDispatcher.js' import {DeviceOwnership} from './support/deviceOwnership.js' import {OwnershipCache} from './support/ownershipCache.js' +import {parseLogcatFilters} from './support/logcatFilters.js' import { UpdateAccessTokenMessage, DeleteUserMessage, @@ -891,10 +892,20 @@ export default (async (options: Options) => { } }) - socket.on('logcat.start', (serial: string, rc: string, data: any) => - runTx(serial, rc, LogcatStartMessage, {filters: data.filters})) - socket.on('logcat.startIos', (serial: string, rc: string, data: any) => - runTx(serial, rc, LogcatStartMessage, {filters: data.filters})) + const startLogcat = (serial: string, rc: string, data: unknown) => { + const filters = parseLogcatFilters((data as {filters?: unknown} | null)?.filters) + if (!filters) { + socket.emit('tx.done', rc, { + source: serial, + success: false, + data: 'invalid_logcat_filters' + }) + return + } + return runTx(serial, rc, LogcatStartMessage, {filters}) + } + socket.on('logcat.start', startLogcat) + socket.on('logcat.startIos', startLogcat) socket.on('logcat.stop', (serial: string, rc: string) => runTx(serial, rc, LogcatStopMessage, {})) socket.on('logcat.stopIos', (serial: string, rc: string) => diff --git a/lib/units/websocket/support/logcatFilters.ts b/lib/units/websocket/support/logcatFilters.ts new file mode 100644 index 0000000000..e6b49137d1 --- /dev/null +++ b/lib/units/websocket/support/logcatFilters.ts @@ -0,0 +1,29 @@ +export interface LogcatFilter { + tag: string + priority: number +} + +const isLogcatFilter = (value: unknown): value is LogcatFilter => { + if (!value || typeof value !== 'object') { + return false + } + const filter = value as Partial + return typeof filter.tag === 'string' && + typeof filter.priority === 'number' && + Number.isInteger(filter.priority) && + filter.priority >= 0 && + filter.priority <= 0xffffffff +} + +// The UI sends one {tag, priority} filter, while the protobuf field is repeated. +// Keep accepting arrays for older clients, and reject malformed payloads before +// protobuf-ts tries to merge them into the repeated field. +export const parseLogcatFilters = (value: unknown): LogcatFilter[] | null => { + if (isLogcatFilter(value)) { + return [value] + } + if (Array.isArray(value) && value.every(isLogcatFilter)) { + return value + } + return null +} diff --git a/test/unit/units/websocket/logcatFilters.test.ts b/test/unit/units/websocket/logcatFilters.test.ts new file mode 100644 index 0000000000..e5bf92a7ad --- /dev/null +++ b/test/unit/units/websocket/logcatFilters.test.ts @@ -0,0 +1,28 @@ +import {describe, expect, it} from 'vitest' +import {parseLogcatFilters} from '../../../../lib/units/websocket/support/logcatFilters.ts' + +describe('parseLogcatFilters', () => { + it('wraps the single filter sent by the UI in the repeated protobuf field', () => { + expect(parseLogcatFilters({tag: '*', priority: 2})).toEqual([ + {tag: '*', priority: 2} + ]) + }) + + it('preserves filter arrays sent by older clients', () => { + const filters = [ + {tag: 'ActivityManager', priority: 4}, + {tag: 'System.err', priority: 6} + ] + expect(parseLogcatFilters(filters)).toEqual(filters) + }) + + it('accepts an empty filter array', () => { + expect(parseLogcatFilters([])).toEqual([]) + }) + + it('rejects malformed filters before protobuf serialization', () => { + expect(parseLogcatFilters({tag: '*', priority: 'verbose'})).toBeNull() + expect(parseLogcatFilters([{tag: '*', priority: 2}, {tag: 1, priority: 3}])).toBeNull() + expect(parseLogcatFilters(undefined)).toBeNull() + }) +})