mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-08 08:58:00 +00:00
1405659dee
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
)
389 lines
12 KiB
C
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);
|
|
}
|
|
}
|
|
}
|
|
}
|