Skip to content

Commit

Permalink
fix(cli): handle overflow in promptSecret (#6318)
Browse files Browse the repository at this point in the history
  • Loading branch information
cosmastech authored Jan 10, 2025
1 parent 6cf1eea commit 0339121
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 2 deletions.
34 changes: 32 additions & 2 deletions cli/prompt_secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const CR = "\r".charCodeAt(0); // ^M - Enter on macOS and Windows (CRLF)
const BS = "\b".charCodeAt(0); // ^H - Backspace on Linux and Windows
const DEL = 0x7f; // ^? - Backspace on macOS
const CLR = encoder.encode("\r\u001b[K"); // Clear the current line
const MOVE_LINE_UP = encoder.encode("\r\u001b[1F"); // Move to previous line

// The `cbreak` option is not supported on Windows
const setRawOptions = Deno.build.os === "windows"
Expand Down Expand Up @@ -52,12 +53,41 @@ export function promptSecret(
return null;
}

const { columns } = Deno.consoleSize();
let previousLength = 0;
// Make the output consistent with the built-in prompt()
message += " ";
const callback = !mask ? undefined : (n: number) => {
output.writeSync(CLR);
output.writeSync(encoder.encode(`${message}${mask.repeat(n)}`));
let line = `${message}${mask.repeat(n)}`;
const currentLength = line.length;
const charsPastLineLength = line.length % columns;

if (line.length > columns) {
line = line.slice(
-1 * (charsPastLineLength === 0 ? columns : charsPastLineLength),
);
}

// If the user has deleted a character
if (currentLength < previousLength) {
// Then clear the current line.
output.writeSync(CLR);
if (charsPastLineLength === 0) {
// And if there's no characters on the current line, return to previous line.
output.writeSync(MOVE_LINE_UP);
}
} else {
// Always jump the cursor back to the beginning of the line unless it's the first character.
if (charsPastLineLength !== 1) {
output.writeSync(CLR);
}
}

output.writeSync(encoder.encode(line));

previousLength = currentLength;
};

output.writeSync(encoder.encode(message));

Deno.stdin.setRaw(true, setRawOptions);
Expand Down
220 changes: 220 additions & 0 deletions cli/prompt_secret_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const decoder = new TextDecoder();
Deno.test("promptSecret() handles CR", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -54,6 +57,9 @@ Deno.test("promptSecret() handles CR", () => {
Deno.test("promptSecret() handles LF", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -98,6 +104,9 @@ Deno.test("promptSecret() handles LF", () => {
Deno.test("promptSecret() handles input", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -155,6 +164,9 @@ Deno.test("promptSecret() handles input", () => {
Deno.test("promptSecret() handles DEL", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -218,6 +230,9 @@ Deno.test("promptSecret() handles DEL", () => {
Deno.test("promptSecret() handles BS", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -281,6 +296,9 @@ Deno.test("promptSecret() handles BS", () => {
Deno.test("promptSecret() handles clear option", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -340,6 +358,9 @@ Deno.test("promptSecret() handles clear option", () => {
Deno.test("promptSecret() handles mask option", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -397,6 +418,9 @@ Deno.test("promptSecret() handles mask option", () => {
Deno.test("promptSecret() handles empty mask option", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -470,6 +494,9 @@ Deno.test("promptSecret() returns null if Deno.stdin.isTerminal() is false", ()
Deno.test("promptSecret() handles null readSync", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand Down Expand Up @@ -500,6 +527,9 @@ Deno.test("promptSecret() handles null readSync", () => {
Deno.test("promptSecret() handles empty readSync", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 80, rows: 20 };
});

const expectedOutput = [
"Please provide the password: ",
Expand All @@ -526,3 +556,193 @@ Deno.test("promptSecret() handles empty readSync", () => {
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSecret() wraps characters wider than console columns", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 5, rows: 20 };
});

const expectedOutput = [
"? ",
"\r\x1b[K",
"? *",
"\r\x1b[K",
"? **",
"\r\x1b[K",
"? ***",
"*",
"\r\x1b[K",
"**",
"\r\x1b[K",
"***",
"\r\x1b[K",
"****",
"\r\x1b[K",
"*****",
"*",
"\r\x1b[K",
"**",
"\n",
];

const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
actualOutput.push(output);
return data.length;
},
);

let readIndex = 0;

const inputs = [
"d",
"e",
"n",
"o",
" ",
"r",
"u",
"l",
"e",
"s",
"\r",
];

stub(
Deno.stdin,
"readSync",
(data: Uint8Array) => {
const input = inputs[readIndex++];
const bytes = encoder.encode(input);
data.set(bytes);
return bytes.length;
},
);

const password = promptSecret("?");

assertEquals(password, "deno rules");
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSecret() returns to previous line when deleting characters", () => {
stub(Deno.stdin, "setRaw");
stub(Deno.stdin, "isTerminal", () => true);
stub(Deno, "consoleSize", () => {
return { columns: 6, rows: 20 };
});

const expectedOutput = [
"? ",
"\r\u001b[K",
"? *",
"\r\u001b[K",
"? **",
"\r\u001b[K",
"? ***",
"\r\u001b[K",
"? ****",
"*",
"\r\u001b[K",
"**",
"\r\u001b[K",
"***",
"\r\u001b[K",
"****",
"\r\u001b[K",
"*****",
"\r\u001b[K",
"******",
"*",
"\r\u001b[K",
"**",
"\r\u001b[K",
"***",
"\r\u001b[K",
"**",
"\r\u001b[K",
"*",
"\r\u001b[K",
"\r\u001b[1F",
"******",
"\r\u001b[K",
"*****",
"\r\u001b[K",
"****",
"\r\u001b[K",
"***",
"\r\u001b[K",
"**",
"\r\u001b[K",
"*",
"\r\u001b[K",
"\r\u001b[1F",
"? ****",
"\n",
];

const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
actualOutput.push(output);
return data.length;
},
);

let readIndex = 0;

const inputs = [
"d",
"e",
"n",
"o",
" ",
"r",
"u",
"l",
"e",
"s",
"!",
"!",
"!",
"\x7f",
"\x7f",
"\x7f",
"\x7f",
"\x7f",
"\x7f",
"\x7f",
"\x7f",
"\x7f",
"\r",
];

stub(
Deno.stdin,
"readSync",
(data: Uint8Array) => {
const input = inputs[readIndex++];
const bytes = encoder.encode(input);
data.set(bytes);
return bytes.length;
},
);

const password = promptSecret("?");

assertEquals(password, "deno");
assertEquals(expectedOutput, actualOutput);
restore();
});

0 comments on commit 0339121

Please sign in to comment.