1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-08 08:58:00 +00:00
putty-source/ldisc.c
Simon Tatham 1405659dee ldisc: fix unwanted double-action of ^U.
In ldisc's line editing mode, pressing ^U is supposed to erase the
current unsent line rather than inserting a literal ^U into the
buffer. In fact, when using a non-Telnet backend, it erases the
line *and* inserts ^U into the buffer!

This happens because it shares a case handler with three other
disruptive control characters (^C, ^\, ^Z), which all also clear the
line-editing buffer before doing their various actions. But in
non-Telnet mode, their actions become literal insertion of themselves,
so the combined effect is to erase the line and them self-insert.

I'm not 100% convinced that was what I actually meant to do with those
characters. But it _certainly_ wasn't what I meant to do with ^U, so
that one at least I should fix right now!

(cherry picked from commit 5ade8c0047)
2023-04-19 14:28:36 +01:00

389 lines
12 KiB
C

/*
* ldisc.c: PuTTY line discipline. Sits between the input coming
* from keypresses in the window, and the output channel leading to
* the back end. Implements echo and/or local line editing,
* depending on what's currently configured.
*/
#include <stdio.h>
#include <ctype.h>
#include <assert.h>
#include "putty.h"
#include "terminal.h"
typedef enum InputType { NORMAL, DEDICATED, NONINTERACTIVE } InputType;
struct input_chunk {
struct input_chunk *next;
InputType type;
size_t size;
};
struct Ldisc_tag {
Terminal *term;
Backend *backend;
Seat *seat;
/*
* When the backend is not reporting true from sendok(), we must
* buffer the input received by ldisc_send(). It's stored in the
* bufchain below, together with a linked list of input_chunk
* blocks storing the extra metadata about special keys and
* interactivity that ldisc_send() receives.
*
* All input is added to this buffer initially, but we then
* process as much of it as possible immediately and hand it off
* to the backend or a TermLineEditor. Anything left stays in this
* buffer until ldisc_check_sendok() is next called, triggering a
* run of the callback that tries again to process the queue.
*/
bufchain input_queue;
struct input_chunk *inchunk_head, *inchunk_tail;
IdempotentCallback input_queue_callback;
/*
* Values cached out of conf.
*/
bool telnet_keyboard, telnet_newline;
int protocol, localecho, localedit;
TermLineEditor *le;
TermLineEditorCallbackReceiver le_rcv;
/* We get one of these communicated to us by
* term_get_userpass_input while it's reading a prompt, so that we
* can push data straight into it */
TermLineEditor *userpass_le;
};
#define ECHOING (ldisc->localecho == FORCE_ON || \
(ldisc->localecho == AUTO && \
(backend_ldisc_option_state(ldisc->backend, LD_ECHO))))
#define EDITING (ldisc->localedit == FORCE_ON || \
(ldisc->localedit == AUTO && \
(backend_ldisc_option_state(ldisc->backend, LD_EDIT))))
static void ldisc_input_queue_callback(void *ctx);
static const TermLineEditorCallbackReceiverVtable ldisc_lineedit_receiver_vt;
#define CTRL(x) (x^'@')
Ldisc *ldisc_create(Conf *conf, Terminal *term, Backend *backend, Seat *seat)
{
Ldisc *ldisc = snew(Ldisc);
memset(ldisc, 0, sizeof(Ldisc));
ldisc->backend = backend;
ldisc->term = term;
ldisc->seat = seat;
bufchain_init(&ldisc->input_queue);
ldisc->input_queue_callback.fn = ldisc_input_queue_callback;
ldisc->input_queue_callback.ctx = ldisc;
bufchain_set_callback(&ldisc->input_queue, &ldisc->input_queue_callback);
if (ldisc->term) {
ldisc->le_rcv.vt = &ldisc_lineedit_receiver_vt;
ldisc->le = lineedit_new(ldisc->term, 0, &ldisc->le_rcv);
}
ldisc_configure(ldisc, conf);
/* Link ourselves into the backend and the terminal */
if (term)
term->ldisc = ldisc;
if (backend)
backend_provide_ldisc(backend, ldisc);
return ldisc;
}
void ldisc_configure(Ldisc *ldisc, Conf *conf)
{
ldisc->telnet_keyboard = conf_get_bool(conf, CONF_telnet_keyboard);
ldisc->telnet_newline = conf_get_bool(conf, CONF_telnet_newline);
ldisc->protocol = conf_get_int(conf, CONF_protocol);
ldisc->localecho = conf_get_int(conf, CONF_localecho);
ldisc->localedit = conf_get_int(conf, CONF_localedit);
unsigned flags = 0;
if (ldisc->protocol == PROT_RAW)
flags |= LE_CRLF_NEWLINE;
if (ldisc->telnet_keyboard)
flags |= LE_INTERRUPT | LE_SUSPEND | LE_ABORT;
lineedit_modify_flags(ldisc->le, ~0U, flags);
}
void ldisc_free(Ldisc *ldisc)
{
bufchain_clear(&ldisc->input_queue);
while (ldisc->inchunk_head) {
struct input_chunk *oldhead = ldisc->inchunk_head;
ldisc->inchunk_head = ldisc->inchunk_head->next;
sfree(oldhead);
}
lineedit_free(ldisc->le);
if (ldisc->term)
ldisc->term->ldisc = NULL;
if (ldisc->backend)
backend_provide_ldisc(ldisc->backend, NULL);
delete_callbacks_for_context(ldisc);
sfree(ldisc);
}
void ldisc_echoedit_update(Ldisc *ldisc)
{
seat_echoedit_update(ldisc->seat, ECHOING, EDITING);
/*
* If we've just turned off local line editing mode, and our
* TermLineEditor had a partial buffer, then send the contents of
* the buffer. Rationale: (a) otherwise you lose data; (b) the
* user quite likely typed the buffer contents _anticipating_ that
* local editing would be turned off shortly, and the event was
* slow arriving.
*/
if (!EDITING)
lineedit_send_line(ldisc->le);
}
void ldisc_provide_userpass_le(Ldisc *ldisc, TermLineEditor *le)
{
/*
* Called by term_get_userpass_input to tell us when it has its
* own TermLineEditor processing a password prompt, so that we can
* inject our input into that instead of putting it into our own
* TermLineEditor or sending it straight to the backend.
*/
ldisc->userpass_le = le;
}
static inline bool is_dedicated_byte(char c, InputType type)
{
switch (type) {
case DEDICATED:
return true;
case NORMAL:
return false;
case NONINTERACTIVE:
/*
* Non-interactive input (e.g. from a paste) doesn't come with
* the ability to distinguish dedicated keypresses like Return
* from generic ones like Ctrl+M. So we just have to make up
* an answer to this question. In particular, we _must_ treat
* Ctrl+M as the Return key, because that's the only way a
* newline can be pasted at all.
*/
return c == '\r';
default:
unreachable("those values should be exhaustive");
}
}
static void ldisc_input_queue_consume(Ldisc *ldisc, size_t size)
{
bufchain_consume(&ldisc->input_queue, size);
while (size > 0) {
size_t thissize = (size < ldisc->inchunk_head->size ?
size : ldisc->inchunk_head->size);
ldisc->inchunk_head->size -= thissize;
size -= thissize;
if (!ldisc->inchunk_head->size) {
struct input_chunk *oldhead = ldisc->inchunk_head;
ldisc->inchunk_head = ldisc->inchunk_head->next;
if (!ldisc->inchunk_head)
ldisc->inchunk_tail = NULL;
sfree(oldhead);
}
}
}
static void ldisc_lineedit_to_terminal(
TermLineEditorCallbackReceiver *rcv, ptrlen data)
{
Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv);
if (ECHOING)
seat_stdout(ldisc->seat, data.ptr, data.len);
}
static void ldisc_lineedit_to_backend(
TermLineEditorCallbackReceiver *rcv, ptrlen data)
{
Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv);
backend_send(ldisc->backend, data.ptr, data.len);
}
static void ldisc_lineedit_special(
TermLineEditorCallbackReceiver *rcv, SessionSpecialCode code, int arg)
{
Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv);
backend_special(ldisc->backend, code, arg);
}
static void ldisc_lineedit_newline(TermLineEditorCallbackReceiver *rcv)
{
Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv);
if (ldisc->protocol == PROT_RAW)
backend_send(ldisc->backend, "\r\n", 2);
else if (ldisc->protocol == PROT_TELNET && ldisc->telnet_newline)
backend_special(ldisc->backend, SS_EOL, 0);
else
backend_send(ldisc->backend, "\r", 1);
}
static const TermLineEditorCallbackReceiverVtable
ldisc_lineedit_receiver_vt = {
.to_terminal = ldisc_lineedit_to_terminal,
.to_backend = ldisc_lineedit_to_backend,
.special = ldisc_lineedit_special,
.newline = ldisc_lineedit_newline,
};
void ldisc_check_sendok(Ldisc *ldisc)
{
queue_idempotent_callback(&ldisc->input_queue_callback);
}
void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive)
{
assert(ldisc->term);
if (interactive) {
/*
* Interrupt a paste from the clipboard, if one was in
* progress when the user pressed a key. This is easier than
* buffering the current piece of data and saving it until the
* terminal has finished pasting, and has the potential side
* benefit of permitting a user to cancel an accidental huge
* paste.
*/
term_nopaste(ldisc->term);
}
InputType type;
if (len < 0) {
/*
* Less than zero means null terminated special string.
*/
len = strlen(vbuf);
type = DEDICATED;
} else {
type = interactive ? NORMAL : NONINTERACTIVE;
}
/*
* Append our data to input_queue, and ensure it's marked with the
* right type.
*/
bufchain_add(&ldisc->input_queue, vbuf, len);
if (!(ldisc->inchunk_tail && ldisc->inchunk_tail->type == type)) {
struct input_chunk *new_chunk = snew(struct input_chunk);
new_chunk->type = type;
new_chunk->size = 0;
new_chunk->next = NULL;
if (ldisc->inchunk_tail)
ldisc->inchunk_tail->next = new_chunk;
else
ldisc->inchunk_head = new_chunk;
ldisc->inchunk_tail = new_chunk;
}
ldisc->inchunk_tail->size += len;
/*
* And process as much of the data immediately as we can.
*/
ldisc_input_queue_callback(ldisc);
}
static void ldisc_input_queue_callback(void *ctx)
{
Ldisc *ldisc = (Ldisc *)ctx;
/*
* Toplevel callback that is triggered whenever the input queue
* lengthens.
*/
while (bufchain_size(&ldisc->input_queue)) {
ptrlen pl = bufchain_prefix(&ldisc->input_queue);
const char *start = pl.ptr, *buf = pl.ptr;
size_t len = (pl.len < ldisc->inchunk_head->size ?
pl.len : ldisc->inchunk_head->size);
InputType type = ldisc->inchunk_head->type;
while (len > 0 && ldisc->userpass_le) {
char c = *buf++;
len--;
bool dedicated = is_dedicated_byte(c, type);
lineedit_input(ldisc->userpass_le, c, dedicated);
}
if (!backend_sendok(ldisc->backend)) {
ldisc_input_queue_consume(ldisc, buf - start);
break;
}
/*
* Either perform local editing, or just send characters.
*/
if (EDITING) {
while (len > 0) {
char c = *buf++;
len--;
bool dedicated = is_dedicated_byte(c, type);
lineedit_input(ldisc->le, c, dedicated);
}
ldisc_input_queue_consume(ldisc, buf - start);
} else {
if (ECHOING)
seat_stdout(ldisc->seat, buf, len);
if (type == DEDICATED && ldisc->protocol == PROT_TELNET) {
while (len > 0) {
char c = *buf++;
len--;
switch (c) {
case CTRL('M'):
if (ldisc->telnet_newline)
backend_special(ldisc->backend, SS_EOL, 0);
else
backend_send(ldisc->backend, "\r", 1);
break;
case CTRL('?'):
case CTRL('H'):
if (ldisc->telnet_keyboard) {
backend_special(ldisc->backend, SS_EC, 0);
break;
}
case CTRL('C'):
if (ldisc->telnet_keyboard) {
backend_special(ldisc->backend, SS_IP, 0);
break;
}
case CTRL('Z'):
if (ldisc->telnet_keyboard) {
backend_special(ldisc->backend, SS_SUSP, 0);
break;
}
default:
backend_send(ldisc->backend, &c, 1);
break;
}
}
ldisc_input_queue_consume(ldisc, buf - start);
} else {
backend_send(ldisc->backend, buf, len);
ldisc_input_queue_consume(ldisc, len);
}
}
}
}