sortix-mirror/utils/pager.c
Juhani Krekelä 2e193b6014 Fix possible overflow when resizing buffers and remove unncessary casts.
A line longer than 2G might overflow the size_t containing the buffer
length on 32-bit platform. Fix is to use reallocarray with second
parameter twice the size of the element to double the buffer size, since
it will error out if there was an overflow.
2021-11-02 02:11:18 +02:00

755 lines
16 KiB
C

/*
* Copyright (c) 2014, 2015, 2016 Jonas 'Sortie' Termansen.
* Copyright (c) 2021 Juhani 'nortti' Krekelä.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* pager.c
* Displays files one page at a time.
*/
#include <sys/ioctl.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <wchar.h>
#define CONTROL_SEQUENCE_MAX 128
enum control_state
{
CONTROL_STATE_NONE = 0,
CONTROL_STATE_CSI,
CONTROL_STATE_COMMAND,
};
struct line
{
char* content;
size_t content_used;
size_t content_length;
};
static struct termios restore_tio;
static bool restore_scrollback;
static int tty_fd;
static bool stdout_is_tty;
static struct winsize winsize;
static mbstate_t in_ps;
static mbstate_t out_ps;
static const char* input_prompt_name;
static size_t possible_lines;
static size_t allowed_lines;
static bool quiting;
static bool flag_raw_control_chars;
static bool flag_color_sequences;
static enum control_state control_state;
static wchar_t control_sequence[CONTROL_SEQUENCE_MAX];
static size_t control_sequence_length;
static bool input_set_color;
static struct line* lines;
static size_t lines_used;
static size_t lines_length;
static enum control_state incoming_control_state;
static struct line* incoming_line;
static size_t incoming_line_width;
static size_t current_line;
static size_t current_line_offset;
static bool allowance_ever_exhausted;
static bool skipping_to_end;
static bool next_bold;
static bool next_underline;
static void exit_restore_tio(void)
{
if ( restore_scrollback )
dprintf(1, "\e[?1049l");
if ( tcsetattr(tty_fd, TCSADRAIN, &restore_tio) < 0 )
warn("tcsetattr");
}
static void init(void)
{
tty_fd = 0;
if ( !isatty(tty_fd) )
{
if ( (tty_fd = open("/dev/tty", O_RDONLY)) < 0 )
err(1, "/dev/tty");
if ( !isatty(tty_fd) )
err(1, "/dev/tty");
}
if ( tcgetattr(tty_fd, &restore_tio) < 0 )
err(1, "tcgetattr");
if ( atexit(exit_restore_tio) < 0 )
err(1, "atexit");
struct termios tio = restore_tio;
tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
if ( tcsetattr(tty_fd, TCSADRAIN, &tio) < 0 )
err(1, "tcsetattr");
if ( (stdout_is_tty = isatty(1)) )
{
if ( ioctl(1, TIOCGWINSZ, &winsize) < 0 )
err(1, "ioctl: TIOCGWINSZ");
possible_lines = winsize.ws_row - 1;
allowed_lines = possible_lines;
const char* term = getenv("TERM");
if ( term &&
strcmp(term, "sortix") != 0 &&
strncmp(term, "sortix-", strlen("sortix-")) != 0 )
{
dprintf(1, "\e[?1049h\e[H");
restore_scrollback = true;
}
}
memset(&in_ps, 0, sizeof(in_ps));
memset(&out_ps, 0, sizeof(out_ps));
}
static char next_char(void)
{
char c;
if ( read(tty_fd, &c, 1) < 1 )
err(1, "/dev/tty");
return c;
}
static void prompt(bool at_end)
{
const char* pre = input_set_color ? "" : "\e[47;30m";
const char* post = input_set_color ? "" : "\e[m";
if ( at_end )
dprintf(1, "%s(END)%s\e[J", pre, post);
else if ( input_prompt_name[0] )
dprintf(1, "%s%s%s\e[J", pre, input_prompt_name, post);
else
dprintf(1, ":");
while ( true )
{
char c;
char buffer[CONTROL_SEQUENCE_MAX + 1];
size_t buffer_used = 0;
buffer[buffer_used++] = c = next_char();
if ( c == '\e' &&
(buffer[buffer_used++] = c = next_char()) == '[' )
{
while ( buffer_used < CONTROL_SEQUENCE_MAX )
{
char c = next_char();
buffer[buffer_used++] = c;
if ( '@' <= c && c <= '~' )
break;
}
}
buffer[buffer_used] = '\0';
if ( !strcmp(buffer, "\n") ||
!strcmp(buffer, "j") ||
!strcmp(buffer, "\x0e") /* ^N */ ||
!strcmp(buffer, "\e[B") /* Down Arrow */ )
{
dprintf(1, "\r\e[J");
allowed_lines++;
return;
}
if ( !strcmp(buffer, "k") ||
!strcmp(buffer, "\x10") /* ^P */ ||
!strcmp(buffer, "\e[A") /* Up Arrow */ )
{
if ( current_line <= possible_lines )
continue;
dprintf(1, "\e[2J\e[H");
current_line -= possible_lines + 1;
current_line_offset = 0;
allowed_lines = possible_lines;
return;
}
if ( !strcmp(buffer, " ") ||
!strcmp(buffer, "f") ||
!strcmp(buffer, "\x06") /* ^F */ ||
!strcmp(buffer, "\x16") /* ^V */ ||
!strcmp(buffer, "\e[6~") /* Page Down */ )
{
dprintf(1, "\r\e[J");
allowed_lines = possible_lines;
return;
}
if ( !strcmp(buffer, "b") ||
!strcmp(buffer, "\x02") /* ^B */ ||
!strcmp(buffer, "\ev") /* ESC-v */ ||
!strcmp(buffer, "\e[5~") /* Page Up */)
{
if ( current_line <= possible_lines )
continue;
size_t distance = possible_lines;
if ( current_line - possible_lines < distance )
distance = current_line - possible_lines;
dprintf(1, "\e[2J\e[H");
current_line -= possible_lines + distance;
current_line_offset = 0;
allowed_lines = possible_lines;
return;
}
if ( !strcmp(buffer, ">") ||
!strcmp(buffer, "G") ||
!strcmp(buffer, "\e[F") /* End */ ||
!strcmp(buffer, "\e[4~") /* End (Linux console) */ )
{
dprintf(1, "\r\e[J");
skipping_to_end = true;
allowed_lines = SIZE_MAX;
return;
}
if ( !strcmp(buffer, "<") ||
!strcmp(buffer, "g") ||
!strcmp(buffer, "\e[H") /* Home */ ||
!strcmp(buffer, "\e[1~") /* Home (Linux console) */ )
{
if ( current_line <= possible_lines )
continue;
dprintf(1, "\e[2J\e[H");
current_line = 0;
current_line_offset = 0;
allowed_lines = possible_lines;
return;
}
if ( !strcmp(buffer, "q") || !strcmp(buffer, "Q") )
{
dprintf(1, "\r\e[J");
quiting = true;
return;
}
}
err(1, "/dev/tty");
}
static void line_push_char(struct line* line, char c)
{
if ( line->content_used == line->content_length )
{
size_t length = line->content_length;
if ( !length )
length = 64;
char* new = reallocarray(line->content, length, 2);
if ( !new )
err(1, "malloc");
line->content = new;
line->content_length = length * 2;
}
line->content[line->content_used++] = c;
}
static void line_push_string(struct line* line, const char* str)
{
for ( size_t i = 0; str[i]; i++ )
line_push_char(line, str[i]);
}
static struct line* continue_line(void)
{
if ( incoming_line )
return incoming_line;
if ( lines_used == lines_length )
{
size_t length = lines_length;
if ( !length )
length = 64;
struct line* new = reallocarray(lines, length, sizeof(struct line) * 2);
if ( !new )
err(1, "malloc");
lines = new;
lines_length = length * 2;
}
incoming_line = &lines[lines_used++];
memset(incoming_line, 0, sizeof(*incoming_line));
incoming_line_width = 0;
return incoming_line;
}
static void finish_line(void)
{
struct line* line = incoming_line;
assert(line);
size_t length = line->content_used ? line->content_used : 1;
char* final_content = realloc(line->content, length);
if ( final_content )
line->content = final_content;
incoming_line = NULL;
incoming_line_width = 0;
}
static struct line* next_line(void)
{
finish_line();
return continue_line();
}
static void push_wchar(wchar_t wc)
{
bool newline = false;
int width;
struct line* line = continue_line();
if ( incoming_control_state == CONTROL_STATE_CSI )
{
incoming_control_state = CONTROL_STATE_NONE;
if ( wc == '[' )
incoming_control_state = CONTROL_STATE_COMMAND;
}
else if ( incoming_control_state == CONTROL_STATE_COMMAND )
{
incoming_control_state = CONTROL_STATE_NONE;
if ( ('0' <= wc && wc <= '9') ||
wc == L';' || wc == L':' || wc == L'?' )
incoming_control_state = CONTROL_STATE_COMMAND;
}
else if ( wc == L'\b' )
{
if ( incoming_line_width )
incoming_line_width--;
while ( line->content_used &&
(line->content[line->content_used-1] & 0xC0) == 0x80 )
line->content_used--;
if ( line->content_used )
{
char c = line->content[--line->content_used];
if ( c == '_' )
next_underline = true;
else if ( c == ' ' )
next_bold = false;
else
next_bold = true;
}
return;
}
else if ( wc == L'\e' )
{
incoming_control_state = CONTROL_STATE_CSI;
}
else if ( wc == L'\n' )
{
newline = true;
}
else if ( wc == L'\t' )
{
if ( winsize.ws_col == incoming_line_width )
line = next_line();
do
{
if ( winsize.ws_col == incoming_line_width )
break;
incoming_line_width++;
} while ( incoming_line_width % 8 != 0 );
}
else if ( wc == L'\r' )
{
incoming_line_width = 0;
}
else if ( wc == 127 )
{
}
else if ( 0 <= (width = wcwidth(wc)) )
{
size_t left = winsize.ws_col - incoming_line_width;
if ( left < (size_t) width )
line = next_line();
incoming_line_width += width;
}
else
{
// TODO: What can cause this and how to handle it?
}
char mb[MB_CUR_MAX];
size_t amount = wcrtomb(mb, wc, &out_ps);
if ( amount != (size_t) -1 )
{
if ( next_bold && next_underline )
line_push_string(line, "\e[1;4m");
else if ( next_bold )
line_push_string(line, "\e[1m");
else if ( next_underline )
line_push_string(line, "\e[4m");
for ( size_t i = 0; i < amount; i++ )
line_push_char(line, mb[i]);
if ( next_bold && next_underline )
line_push_string(line, "\e[22;24m");
else if ( next_bold )
line_push_string(line, "\e[22m");
else if ( next_underline )
line_push_string(line, "\e[24m");
next_bold = false;
next_underline = false;
}
if ( newline )
finish_line();
}
static bool push_wchar_is_escaped(wchar_t wc)
{
if ( wc == '\b' &&
(flag_raw_control_chars || flag_color_sequences) )
return false;
return wc < 32 && wc != L'\t' && wc != L'\n';
}
static void push_wchar_escape(wchar_t wc)
{
if ( push_wchar_is_escaped(wc) )
{
push_wchar(L'^');
//push_wchar(L'\b');
//push_wchar(L'^');
push_wchar(L'@' + wc);
//push_wchar(L'\b');
//push_wchar(L'@' + wc);
}
else
{
push_wchar(wc);
}
}
static void control_sequence_begin(void)
{
control_sequence_length = 0;
}
static void control_sequence_accept(void)
{
for ( size_t i = 0; i < control_sequence_length; i++ )
push_wchar(control_sequence[i]);
control_sequence_length = 0;
control_state = CONTROL_STATE_NONE;
}
static void control_sequence_reject(void)
{
for ( size_t i = 0; i < control_sequence_length; i++ )
push_wchar_escape(control_sequence[i]);
control_sequence_length = 0;
control_state = CONTROL_STATE_NONE;
}
static void control_sequence_push(wchar_t wc)
{
if ( flag_raw_control_chars )
return push_wchar(wc);
if ( CONTROL_SEQUENCE_MAX <= control_sequence_length )
{
control_sequence_reject();
push_wchar_escape(wc);
return;
}
control_sequence[control_sequence_length++] = wc;
}
static void control_sequence_finish(wchar_t wc)
{
control_sequence_push(wc);
if ( control_state == CONTROL_STATE_NONE )
return;
if ( wc == L'm' )
{
input_set_color = true;
return control_sequence_accept();
}
if ( !flag_raw_control_chars )
return control_sequence_reject();
control_sequence_accept();
}
static void push_wchar_filter(wchar_t wc)
{
if ( wc == L'\e' &&
(flag_raw_control_chars || flag_color_sequences) &&
control_state == CONTROL_STATE_NONE )
{
control_sequence_begin();
control_sequence_push(wc);
control_state = CONTROL_STATE_CSI;
return;
}
else if ( control_state == CONTROL_STATE_CSI )
{
if ( wc == L'[' )
{
control_sequence_push(wc);
control_state = CONTROL_STATE_COMMAND;
return;
}
control_sequence_reject();
}
else if ( control_state == CONTROL_STATE_COMMAND )
{
if ( ('0' <= wc && wc <= '9') ||
wc == L';' || wc == L':' || wc == L'?' )
{
control_sequence_push(wc);
return;
}
control_sequence_finish(wc);
return;
}
push_wchar_escape(wc);
}
static void push_byte(unsigned char byte)
{
if ( quiting )
return;
wchar_t wc;
size_t amount = mbrtowc(&wc, (const char*) &byte, 1, &in_ps);
if ( amount == (size_t) -2 )
return;
if ( amount == (size_t) -1 )
{
wc = 0xFFFD /* REPLACEMENT CHARACTER */;
memset(&in_ps, 0, sizeof(in_ps));
}
push_wchar_filter(wc);
}
static bool read_fd(int fd, const char* fdpath)
{
unsigned char buffer[4096];
ssize_t amount = read(fd, buffer, sizeof(buffer));
if ( amount < 0 )
err(1, "%s", fdpath);
for ( ssize_t i = 0; i < amount; i++ )
push_byte(buffer[i]);
return amount != 0;
}
static void simple_fd(int fd, const char* fdpath)
{
unsigned char buffer[4096];
ssize_t amount = 0;
while ( 0 < (amount = read(fd, buffer, sizeof(buffer))) )
{
ssize_t sofar = 0;
while ( sofar < amount )
{
ssize_t done = write(1, buffer + sofar, amount - sofar);
if ( done < 0 )
err(1, "<stdout>");
sofar += done;
}
}
if ( amount < 0 )
err(1, "%s", fdpath);
}
static bool can_page(void)
{
if ( current_line + 1 == lines_used )
{
struct line* line = &lines[current_line];
return current_line_offset < line->content_used;
}
return current_line + 1 < lines_used;
}
static void page(void)
{
struct line* line = &lines[current_line];
if ( current_line_offset < line->content_used )
{
const char* buffer = line->content + current_line_offset;
size_t amount = line->content_used - current_line_offset;
size_t sofar = 0;
while ( sofar < amount )
{
ssize_t done = write(1, buffer + sofar, amount - sofar);
if ( done < 0 )
err(1, "<stdout>");
sofar += done;
}
current_line_offset = line->content_used;
}
if ( current_line + 1 < lines_used )
{
if ( allowed_lines != SIZE_MAX )
allowed_lines--;
current_line++;
current_line_offset = 0;
}
}
static void push_fd(int fd, const char* fdpath)
{
if ( quiting )
return;
if ( !strcmp(fdpath, "<stdin>") )
input_prompt_name = "";
else
input_prompt_name = fdpath;
// TODO: In this case, we should disable echoing and read from the terminal
// anyways. Remember to enable it again.
if ( isatty(fd) )
errx(1, "/dev/tty: Is a terminal");
if ( !stdout_is_tty )
return simple_fd(fd, fdpath);
bool eof = false;
while ( !quiting )
{
if ( !skipping_to_end )
{
if ( allowed_lines == 0 )
{
allowance_ever_exhausted = true;
prompt(false);
continue;
}
if ( can_page() )
{
page();
continue;
}
}
if ( eof )
break;
if ( !read_fd(fd, fdpath) )
eof = true;
}
}
static void push_path(const char* path)
{
if ( quiting )
return;
if ( !strcmp(path, "-") )
return push_fd(0, "<stdin>");
int fd = open(path, O_RDONLY);
if ( fd < 0 )
err(1, "%s", path);
push_fd(fd, path);
close(fd);
}
static void compact_arguments(int* argc, char*** argv)
{
for ( int i = 0; i < *argc; i++ )
{
while ( i < *argc && !(*argv)[i] )
{
for ( int n = i; n < *argc; n++ )
(*argv)[n] = (*argv)[n+1];
(*argc)--;
}
}
}
int main(int argc, char* argv[])
{
setlocale(LC_ALL, "");
if ( getenv("LESS") )
{
const char* options = getenv("LESS");
char c;
while ( (c = *options++) )
{
switch ( c )
{
case '-': break;
case 'r': flag_raw_control_chars = true; break;
case 'R': flag_color_sequences = true; break;
default: break;
}
}
}
for ( int i = 1; i < argc; i++ )
{
const char* arg = argv[i];
if ( arg[0] != '-' || !arg[1] )
continue;
argv[i] = NULL;
if ( !strcmp(arg, "--") )
break;
if ( arg[1] != '-' )
{
char c;
while ( (c = *++arg) ) switch ( c )
{
case 'r': flag_raw_control_chars = true; break;
case 'R': flag_color_sequences = true; break;
default:
errx(1, "unknown option -- '%c'", c);
}
}
else
errx(1, "unknown option: %s", arg);
}
compact_arguments(&argc, &argv);
init();
if ( argc == 1 )
{
if ( tty_fd == 0 )
errx(1, "missing file operand");
push_fd(0, "<stdin>");
}
else for ( int i = 1; i < argc; i++ )
{
push_path(argv[i]);
}
while ( stdout_is_tty &&
(allowance_ever_exhausted || restore_scrollback) &&
!quiting )
{
if ( skipping_to_end )
{
dprintf(1, "\e[2J\e[H");
size_t line = 0;
if ( possible_lines <= lines_used )
line = lines_used - possible_lines;
current_line = line;
current_line_offset = 0;
allowed_lines = possible_lines;
skipping_to_end = false;
}
bool cant_page = !can_page();
if ( cant_page || allowed_lines == 0 )
{
prompt(cant_page);
continue;
}
page();
}
return 0;
}