From 93235570d03047873d16f8cd2b2dca9d518649a1 Mon Sep 17 00:00:00 2001 From: Michael Ficocelli Date: Mon, 28 Aug 2023 14:26:25 -0400 Subject: [PATCH] TERMINAL: Prevent recursive aliases from being resolved. (#741) --- src/Alias.ts | 78 ++++++++++++++++++++--------------- test/jest/Alias/Alias.test.ts | 52 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 test/jest/Alias/Alias.test.ts diff --git a/src/Alias.ts b/src/Alias.ts index 2d7b66020..f18daa6a8 100644 --- a/src/Alias.ts +++ b/src/Alias.ts @@ -65,39 +65,51 @@ export function removeAlias(name: string): boolean { /** * Returns the original string with any aliases substituted in. * Aliases are only applied to "whole words", one level deep + * @param origCommand the original command string */ export function substituteAliases(origCommand: string): string { - const commandArray = origCommand.split(" "); - if (commandArray.length > 0) { - // For the alias and unalias commands, don't substitute - if (commandArray[0] === "unalias" || commandArray[0] === "alias") { - return commandArray.join(" "); - } - - let somethingSubstituted = true; - let depth = 0; - let lastAlias; - - while (somethingSubstituted && depth < 10) { - depth++; - somethingSubstituted = false; - const alias = Aliases.get(commandArray[0])?.split(" "); - if (alias !== undefined) { - somethingSubstituted = true; - commandArray.splice(0, 1, ...alias); - //commandArray[0] = alias; - } - for (let i = 0; i < commandArray.length; ++i) { - const alias = GlobalAliases.get(commandArray[i])?.split(" "); - if (alias !== undefined && (commandArray[i] != lastAlias || somethingSubstituted)) { - somethingSubstituted = true; - lastAlias = commandArray[i]; - commandArray.splice(i, 1, ...alias); - i += alias.length - 1; - //commandArray[i] = alias; - } - } - } - } - return commandArray.join(" "); + return applyAliases(origCommand); +} + +/** + * Recursively evaluates aliases and applies them to the command string, + * unless there are any reference loops or the reference chain is too deep + * @param origCommand the original command string + * @param depth the current recursion depth + * @param currentlyProcessingAliases any aliases that have been applied in the recursive evaluation leading to this point + * @return { string } the provided command with all of its referenced aliases evaluated + */ +function applyAliases(origCommand: string, depth = 0, currentlyProcessingAliases: string[] = []) { + if (!origCommand) { + return origCommand; + } + const commandArray = origCommand.split(" "); + + // Do not apply aliases when defining a new alias + if (commandArray[0] === "unalias" || commandArray[0] === "alias") { + return commandArray.join(" "); + } + + // First get non-global aliases, and recursively apply them + // (unless there are any reference loops or the reference chain is too deep) + const localAlias = Aliases.get(commandArray[0]); + if (localAlias && !currentlyProcessingAliases.includes(localAlias)) { + const appliedAlias = applyAliases(localAlias, depth + 1, [commandArray[0], ...currentlyProcessingAliases]); + commandArray.splice(0, 1, ...appliedAlias.split(" ")); + } + + // Once local aliasing is complete (or if none are present) handle any global aliases + const processedCommands = commandArray.reduce((resolvedCommandArray: string[], command) => { + const globalAlias = GlobalAliases.get(command); + if (globalAlias && !currentlyProcessingAliases.includes(globalAlias)) { + const appliedAlias = applyAliases(globalAlias, depth + 1, [command, ...currentlyProcessingAliases]); + resolvedCommandArray.push(appliedAlias); + } else { + // If there is no alias, or if the alias has a circular reference, leave the command as-is + resolvedCommandArray.push(command); + } + return resolvedCommandArray; + }, []); + + return processedCommands.join(" "); } diff --git a/test/jest/Alias/Alias.test.ts b/test/jest/Alias/Alias.test.ts new file mode 100644 index 000000000..d5f594fb7 --- /dev/null +++ b/test/jest/Alias/Alias.test.ts @@ -0,0 +1,52 @@ +import { substituteAliases, parseAliasDeclaration } from "../../../src/Alias"; +describe("substituteAliases Tests", () => { + it("Should gracefully handle recursive local aliases", () => { + parseAliasDeclaration("recursiveAlias=b"); + parseAliasDeclaration("b=c"); + parseAliasDeclaration("c=d"); + parseAliasDeclaration("d=recursiveAlias"); + + const result = substituteAliases("recursiveAlias"); + expect(result).toEqual("d"); + }); + + it("Should only change local aliases if they are the start of the command", () => { + parseAliasDeclaration("a=b"); + parseAliasDeclaration("b=c"); + parseAliasDeclaration("c=d"); + parseAliasDeclaration("d=e"); + + const result = substituteAliases("a b c d"); + expect(result).toEqual("e b c d"); + }); + + it("Should gracefully handle recursive global aliases", () => { + parseAliasDeclaration("a=b", true); + parseAliasDeclaration("b=c", true); + parseAliasDeclaration("c=d", true); + parseAliasDeclaration("d=a", true); + + const result = substituteAliases("a b c d"); + expect(result).toEqual("d a b c"); + }); + + it("Should gracefully handle recursive mixed local and global aliases", () => { + parseAliasDeclaration("recursiveAlias=b", true); + parseAliasDeclaration("b=c", false); + parseAliasDeclaration("c=d", true); + parseAliasDeclaration("d=recursiveAlias", false); + + const result = substituteAliases("recursiveAlias"); + expect(result).toEqual("d"); + }); + + it("Should replace chained aliases", () => { + parseAliasDeclaration("a=b", true); + parseAliasDeclaration("b=c", true); + parseAliasDeclaration("c=d", true); + parseAliasDeclaration("d=e", true); + + const result = substituteAliases("a"); + expect(result).toEqual("e"); + }); +});