diff --git a/package-lock.json b/package-lock.json index 2de28cc..4d0a86b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "frida-cshell", - "version": "1.5.1", + "version": "1.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frida-cshell", - "version": "1.5.1", + "version": "1.5.2", "devDependencies": { "@eslint/js": "^9.10.0", "@types/frida-gum": "^18.7", diff --git a/package.json b/package.json index 9534456..e50da01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frida-cshell", - "version": "1.5.1", + "version": "1.5.2", "description": "Frida's CShell", "scripts": { "prepare": "npm run build && npm run version && npm run package && npm run copy", diff --git a/src/cmdlets/development/js.ts b/src/cmdlets/development/js.ts index 72e4dd4..6d35e40 100644 --- a/src/cmdlets/development/js.ts +++ b/src/cmdlets/development/js.ts @@ -65,6 +65,7 @@ import { TraceUniqueBlockCmdLet, TraceCoverageCmdLet, } from '../trace/trace.js'; +import { MacroCmdLet } from '../misc/macro.js'; export class JsCmdLet extends CmdLet { name = 'js'; @@ -117,6 +118,7 @@ js path - load commandlet JS script LdCmdLet: LdCmdLet, Line: Line, LogCmdLet: LogCmdLet, + MacroCmdLet: MacroCmdLet, Mem: Mem, MemoryBps: MemoryBps, ModCmdLet: ModCmdLet, diff --git a/src/cmdlets/misc/macro.ts b/src/cmdlets/misc/macro.ts new file mode 100644 index 0000000..a655c3a --- /dev/null +++ b/src/cmdlets/misc/macro.ts @@ -0,0 +1,128 @@ +import { CmdLet } from '../../commands/cmdlet.js'; +import { Command } from '../../commands/command.js'; +import { Input, InputInterceptLine } from '../../io/input.js'; +import { Output } from '../../io/output.js'; +import { Parser } from '../../io/parser.js'; +import { Token } from '../../io/token.js'; +import { Macro, Macros } from '../../macros/macros.js'; +import { Var } from '../../vars/var.js'; +import { Vars } from '../../vars/vars.js'; + +export class MacroCmdLet extends CmdLet implements InputInterceptLine { + name = 'm'; + category = 'misc'; + help = 'manage macros'; + private static readonly USAGE: string = `Usage: m +m - show all macros + +m name - create, modify or display a macro + name the name of the macro + +m name ${CmdLet.DELETE_CHAR} - delete a macro + name the name of the macro to delete`; + + private current: string | null = null; + private commands: string[] = []; + + public runSync(tokens: Token[]): Var { + const retWithNameAndHash = this.runDelete(tokens); + if (retWithNameAndHash !== null) return retWithNameAndHash; + + const retWithNameAndPointer = this.runSet(tokens); + if (retWithNameAndPointer !== null) return retWithNameAndPointer; + + const retWithName = this.runShow(tokens); + if (retWithName !== null) return retWithName; + + return this.usage(); + } + + private runDelete(tokens: Token[]): Var | null { + const vars = this.transform(tokens, [this.parseLiteral, this.parseDelete]); + if (vars === null) return null; + const [name, _] = vars as [string, string]; + + const macro = Macros.delete(name); + if (macro === null) { + Output.writeln(`macro ${Output.green(name)} not set`); + } else { + Output.writeln(`deleted macro ${Output.green(name)}`); + } + return Var.ZERO; + } + + private runSet(tokens: Token[]): Var | null { + const vars = this.transform(tokens, [this.parseLiteral]); + if (vars === null) return null; + const [name] = vars as [string]; + this.commands = []; + this.current = name; + const macro = Macros.get(name); + if (macro === null) { + Output.writeln(`Creating macro '${Output.green(name)}'`); + } else { + Output.writeln(`Modifying macro '${Output.green(name)}'`); + Output.writeln(macro.toString()); + } + Input.setInterceptLine(this); + return Var.ZERO; + } + + addLine(line: string): void { + this.commands.push(line); + } + + clear(): void { + if (this.current != null) { + const macro = new Macro(this.current, []); + Macros.set(macro); + } + } + + done(): void { + if (this.current != null) { + const macro = new Macro(this.current, this.commands); + Macros.set(macro); + } + } + + abort(): void {} + + private runShow(tokens: Token[]): Var | null { + if (tokens.length !== 0) return null; + Output.writeln(Output.blue('Macros:')); + for (const macro of Macros.all()) { + Output.writeln(`${Output.green(macro.name)}:`, true); + + Output.writeln(macro.toString(), true); + + Output.writeln(); + } + return Var.ZERO; + } + + public usage(): Var { + Output.writeln(MacroCmdLet.USAGE); + return Var.ZERO; + } + + public static run(macro: Macro): Var { + let ret = Var.ZERO; + for (const command of macro.commands) { + if (command.length === 0) continue; + if (command.charAt(0) === '#') continue; + + Output.writeln(`${Output.bold(Input.PROMPT)}${command}`); + + if (command.trim().length === 0) continue; + + const parser = new Parser(command.toString()); + const tokens = parser.tokenize(); + ret = Command.runSync(tokens); + Vars.setRet(ret); + Output.writeRet(); + Output.writeln(); + } + return ret; + } +} diff --git a/src/commands/cmdlets.ts b/src/commands/cmdlets.ts index d171be2..0619693 100644 --- a/src/commands/cmdlets.ts +++ b/src/commands/cmdlets.ts @@ -56,6 +56,7 @@ import { TraceUniqueBlockCmdLet, TraceCoverageCmdLet, } from '../cmdlets/trace/trace.js'; +import { MacroCmdLet } from '../cmdlets/misc/macro.js'; export class CmdLets { private static byName: Map = new Map(); @@ -89,6 +90,7 @@ export class CmdLets { this.registerCmdletType(LogCmdLet); this.registerCmdletType(OrCmdLet); this.registerCmdletType(ReadCmdLet); + this.registerCmdletType(MacroCmdLet); this.registerCmdletType(ModCmdLet); this.registerCmdletType(MulCmdLet); this.registerCmdletType(NotCmdLet); diff --git a/src/commands/command.ts b/src/commands/command.ts index 64f8923..e3a0721 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -4,15 +4,23 @@ import { Format } from '../misc/format.js'; import { Var } from '../vars/var.js'; import { Token } from '../io/token.js'; import { CmdLet } from './cmdlet.js'; +import { Macro, Macros } from '../macros/macros.js'; +import { MacroCmdLet } from '../cmdlets/misc/macro.js'; export class Command { + private static readonly MACRO_PREFIX: string = '!'; public static async run(tokens: Token[]): Promise { const cmdlet = this.getCmdlet(tokens); - if (cmdlet === null) { - return this.runFunction(tokens); - } else { + if (cmdlet !== null) { return cmdlet.run(tokens.slice(1)); } + + const macro = this.getMacro(tokens); + if (macro !== null) { + return MacroCmdLet.run(macro); + } + + return this.runFunction(tokens); } public static runSync(tokens: Token[]): Var { @@ -30,6 +38,17 @@ export class Command { return CmdLets.getByName(t0.getLiteral()); } + private static getMacro(tokens: Token[]): Macro | null { + if (tokens.length === 0) throw new Error('failed to tokenize macro'); + const t0 = tokens[0] as Token; + const name = t0.getLiteral(); + if (!name.startsWith(Command.MACRO_PREFIX)) return null; + if (name.length === 1) throw new Error('macro name not supplied'); + const macro = Macros.get(name.slice(1)); + if (macro === null) throw new Error(`failed to recognozie macro ${Output.green(name)}`); + return macro; + } + private static runFunction(tokens: Token[]): Var { if (tokens.length === 0) throw new Error('failed to tokenize command'); const t0 = tokens[0] as Token; diff --git a/src/macros/macros.ts b/src/macros/macros.ts new file mode 100644 index 0000000..37efa74 --- /dev/null +++ b/src/macros/macros.ts @@ -0,0 +1,50 @@ +import { Output } from '../io/output.js'; + +export class Macro { + private readonly _name: string; + private readonly _commands: string[] = []; + + constructor(name: string, commands: string[]) { + this._name = name; + this._commands = commands; + } + + public get name(): string { + return this._name; + } + + public get commands(): string[] { + return this._commands; + } + + public toString(): string { + return this._commands + .map(l => Output.writeln(` - ${Output.yellow(l)}`)) + .join('\n'); + } +} + +export class Macros { + private static map: Map = new Map(); + + public static get(name: string): Macro | null { + return this.map.get(name) ?? null; + } + + public static set(macro: Macro) { + this.map.set(macro.name, macro); + } + + public static delete(name: string): Macro | null { + const macro = this.map.get(name); + if (macro === undefined) return null; + this.map.delete(name); + return macro; + } + + public static all(): Macro[] { + return Array.from(this.map.entries()) + .sort(([k1, _v1], [k2, _v2]) => k1.localeCompare(k2)) + .map(([k, v]) => v); + } +}