commit d3e1ab0c5f6f6f37e4897419ae6c3591762d2f59 from: Omar Polo date: Thu Nov 24 18:54:38 2022 UTC kamiftp: add completions This adds completions when building with readline. kamiftp now completes command name, special arguments (on/off), local paths (default readline behaviour) and remote paths. This also tells readline how to handle quotes and escape characters. commit - f99c17f9596718e1ec4df58727f81bec72339110 commit + d3e1ab0c5f6f6f37e4897419ae6c3591762d2f59 blob - 27a8d5b6fe8fb7ec4a46f14d23ab8b288f230dc2 blob + ee3cc466548e596fe25baf99a0aed19fec566aa7 --- kamiftp/Makefile.am +++ kamiftp/Makefile.am @@ -1,6 +1,7 @@ bin_PROGRAMS = kamiftp kamiftp_SOURCES=ftp.c \ + rl.c \ $(top_srcdir)/lib/9pclib.c \ $(top_srcdir)/lib/9pclib.h \ $(top_srcdir)/lib/kami.h \ blob - 553ad8118c5fc9b3d53743ec7f13de1be0142495 blob + 733177d3c2233b99bef23a212c437ddaab83ba02 --- kamiftp/ftp.c +++ kamiftp/ftp.c @@ -42,15 +42,13 @@ #include #endif -#ifndef nitems -#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) -#endif - #include "kami.h" #include "utils.h" #include "log.h" #include "9pclib.h" +#include "kamiftp.h" + #define TMPFSTR "/tmp/kamiftp.XXXXXXXXXX" #define TMPFSTRLEN sizeof(TMPFSTR) @@ -65,6 +63,7 @@ struct evbuffer *buf; struct evbuffer *dirbuf; uint32_t msize; int bell; +time_t now; volatile sig_atomic_t resized; int tty_p; @@ -79,34 +78,7 @@ struct progress { int pwdfid; #define ASSERT_EMPTYBUF() assert(EVBUFFER_LENGTH(buf) == 0) - -#if !HAVE_READLINE -char * -readline(const char *prompt) -{ - char *ch, *line = NULL; - size_t linesize = 0; - ssize_t linelen; - - printf("%s", prompt); - fflush(stdout); - - linelen = getline(&line, &linesize, stdin); - if (linelen == -1) - return NULL; - if ((ch = strchr(line, '\n')) != NULL) - *ch = '\0'; - return line; -} - -void -add_history(const char *line) -{ - return; -} -#endif - static char * read_line(const char *prompt) { @@ -1034,7 +1006,114 @@ prepare_wstat(struct np_stat *st) st->muid = NULL; } -static void +static int +print_dirent(const struct np_stat *st) +{ + time_t mtime; + struct tm *tm; + const char *timfmt; + char fmt[FMT_SCALED_STRSIZE], tim[13]; + + if (fmt_scaled(st->length, fmt) == -1) + strlcpy(fmt, "xxx", sizeof(fmt)); + + mtime = st->mtime; + + if (now > mtime && (now - mtime) < 365/2 * 24 * 12 * 60) + timfmt = "%b %e %R"; + else + timfmt = "%b %e %Y"; + + if ((tm = localtime(&mtime)) == NULL || + strftime(tim, sizeof(tim), timfmt, tm) == 0) + strlcpy(tim, "unknown", sizeof(tim)); + + if (st->qid.type & QTDIR) + printf("d"); + else + printf("-"); + printf("%s", pp_perm(st->mode >> 6)); + printf("%s", pp_perm(st->mode >> 3)); + printf("%s", pp_perm(st->mode)); + printf(" %8s %12s %s%s\n", fmt, tim, st->name, + st->qid.type & QTDIR ? "/" : ""); + + return 0; +} + +int +dir_listing(const char *path, int (*fn)(const struct np_stat *), + int printerr) +{ + struct qid qid; + struct np_stat st; + uint64_t off = 0; + uint32_t len; + int nfid, miss, r; + char *errstr; + + now = time(NULL); + nfid = nextfid(); + + errstr = walk_path(pwdfid, nfid, path, &miss, &qid); + if (errstr != NULL) { + if (printerr) + printf("%s: %s\n", path, errstr); + free(errstr); + return -1; + } + if (miss) { + if (printerr) + printf("%s: No such file or directory\n", path); + return -1; + } + if (!(qid.type & QTDIR)) { + if (printerr) + printf("%s: not a directory\n", path); + do_clunk(nfid); + return -1; + } + + do_open(nfid, KOREAD); + evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf)); + + for (;;) { + tread(nfid, off, msize - IOHDRSZ); + do_send(); + recv_msg(); + expect2(Rread, iota_tag); + + len = np_read32(buf); + if (len == 0) + break; + + evbuffer_add_buffer(dirbuf, buf); + off += len; + + ASSERT_EMPTYBUF(); + } + + while (EVBUFFER_LENGTH(dirbuf) != 0) { + if (np_read_stat(dirbuf, &st) == -1) + errx(1, "invalid stat struct read"); + + r = fn(&st); + + free(st.name); + free(st.uid); + free(st.gid); + free(st.muid); + + if (r == -1) + break; + } + + evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf)); + do_clunk(nfid); + return 0; +} + +void cmd_bell(int argc, const char **argv) { if (argc == 0) { @@ -1065,7 +1144,7 @@ usage: printf("bell [on | off]\n"); } -static void +void cmd_bye(int argc, const char **argv) { log_warnx("bye\n"); @@ -1073,7 +1152,7 @@ cmd_bye(int argc, const char **argv) exit(0); } -static void +void cmd_cd(int argc, const char **argv) { struct qid qid; @@ -1104,7 +1183,7 @@ cmd_cd(int argc, const char **argv) pwdfid = nfid; } -static void +void cmd_edit(int argc, const char **argv) { struct qid qid; @@ -1170,7 +1249,7 @@ end: unlink(sfn); } -static void +void cmd_get(int argc, const char **argv) { struct qid qid; @@ -1216,7 +1295,7 @@ cmd_get(int argc, const char **argv) close(fd); } -static void +void cmd_hexdump(int argc, const char **argv) { if (argc == 0) { @@ -1247,7 +1326,7 @@ usage: puts("usage: hexdump [on | off]"); } -static void +void cmd_lcd(int argc, const char **argv) { const char *dir; @@ -1269,7 +1348,7 @@ cmd_lcd(int argc, const char **argv) printf("cd: %s: %s\n", dir, strerror(errno)); } -static void +void cmd_lpwd(int argc, const char **argv) { char path[PATH_MAX]; @@ -1287,110 +1366,18 @@ cmd_lpwd(int argc, const char **argv) printf("%s\n", path); } -static void +void cmd_ls(int argc, const char **argv) { - struct qid qid; - struct np_stat st; - time_t now, mtime; - struct tm *tm; - uint64_t off = 0; - uint32_t len; - int nfid, miss; - const char *timfmt; - char fmt[FMT_SCALED_STRSIZE], tim[13], *errstr; - if (argc > 1) { puts("usage: ls [path]"); return; } - now = time(NULL); - - nfid = nextfid(); - if (argc == 0) { - if ((errstr = dup_fid(pwdfid, nfid)) != NULL) { - printf(".: %s\n", errstr); - free(errstr); - return; - } - } else { - errstr = walk_path(pwdfid, nfid, argv[0], &miss, &qid); - if (errstr != NULL) { - printf("%s: %s\n", argv[0], errstr); - free(errstr); - return; - } - if (miss) { - printf("%s: No such file or directory\n", - argv[0]); - return; - } - if (!(qid.type & QTDIR)) { - printf("%s: not a directory\n", argv[0]); - do_clunk(nfid); - return; - } - } - - do_open(nfid, KOREAD); - - evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf)); - - for (;;) { - tread(nfid, off, msize - IOHDRSZ); - do_send(); - recv_msg(); - expect2(Rread, iota_tag); - - len = np_read32(buf); - if (len == 0) - break; - - evbuffer_add_buffer(dirbuf, buf); - off += len; - - ASSERT_EMPTYBUF(); - } - - while (EVBUFFER_LENGTH(dirbuf) != 0) { - if (np_read_stat(dirbuf, &st) == -1) - errx(1, "invalid stat struct read"); - - if (fmt_scaled(st.length, fmt) == -1) - strlcpy(fmt, "xxx", sizeof(fmt)); - - mtime = st.mtime; - - if (now > mtime && (now - mtime) < 365/2 * 24 * 12 * 60) - timfmt = "%b %e %R"; - else - timfmt = "%b %e %Y"; - - if ((tm = localtime(&mtime)) == NULL || - strftime(tim, sizeof(tim), timfmt, tm) == 0) - strlcpy(tim, "unknown", sizeof(tim)); - - if (st.qid.type & QTDIR) - printf("d"); - else - printf("-"); - printf("%s", pp_perm(st.mode >> 6)); - printf("%s", pp_perm(st.mode >> 3)); - printf("%s", pp_perm(st.mode)); - printf(" %8s %12s %s%s\n", fmt, tim, st.name, - st.qid.type & QTDIR ? "/" : ""); - - free(st.name); - free(st.uid); - free(st.gid); - free(st.muid); - } - - do_clunk(nfid); + dir_listing(argc == 0 ? "." : argv[0], print_dirent, 1); } -static void +void cmd_page(int argc, const char **argv) { struct qid qid; @@ -1436,7 +1423,7 @@ cmd_page(int argc, const char **argv) unlink(sfn); } -static void +void cmd_pipe(int argc, const char **argv) { struct qid qid; @@ -1487,7 +1474,7 @@ cmd_pipe(int argc, const char **argv) waitpid(pid, &status, 0); } -static void +void cmd_put(int argc, const char **argv) { struct qid qid; @@ -1515,7 +1502,7 @@ cmd_put(int argc, const char **argv) close(fd); } -static void +void cmd_rename(int argc, const char **argv) { struct np_stat st; @@ -1551,7 +1538,7 @@ cmd_rename(int argc, const char **argv) do_clunk(nfid); } -static void +void cmd_rm(int argc, const char **argv) { struct qid qid; @@ -1584,7 +1571,7 @@ cmd_rm(int argc, const char **argv) } } -static void +void cmd_verbose(int argc, const char **argv) { if (argc == 0) { @@ -1618,31 +1605,11 @@ usage: static void excmd(int argc, const char **argv) { - struct cmd { - const char *name; - void (*fn)(int, const char **); - } cmds[] = { - {"bell", cmd_bell}, - {"bye", cmd_bye}, - {"cd", cmd_cd}, - {"edit", cmd_edit}, - {"get", cmd_get}, - {"hexdump", cmd_hexdump}, - {"lcd", cmd_lcd}, - {"lpwd", cmd_lpwd}, - {"ls", cmd_ls}, - {"page", cmd_page}, - {"pipe", cmd_pipe}, - {"put", cmd_put}, - {"quit", cmd_bye}, - {"rename", cmd_rename}, - {"rm", cmd_rm}, - {"verbose", cmd_verbose}, - }; size_t i; if (argc == 0) return; + for (i = 0; i < nitems(cmds); ++i) { if (!strcmp(cmds[i].name, *argv)) { cmds[i].fn(argc-1, argv+1); @@ -1875,6 +1842,7 @@ main(int argc, char **argv) if (path) cd_or_fetch(path, outfile); + compl_setup(); for (;;) { int argc; char *line, *argv[16] = {0}, **ap; blob - /dev/null blob + b249f79672dbcddf9b6e871ea9a0daab7a4513b6 (mode 644) --- /dev/null +++ kamiftp/kamiftp.h @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022 Omar Polo + * + * 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. + */ + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +enum ftp_cmd { + CMD_UNKNOWN, + CMD_BELL, + CMD_BYE, + CMD_CD, + CMD_EDIT, + CMD_GET, + CMD_HEXDUMP, + CMD_LCD, + CMD_LPWD, + CMD_LS, + CMD_PAGE, + CMD_PIPE, + CMD_PUT, + CMD_RENAME, + CMD_RM, + CMD_VERBOSE, +}; + +void compl_setup(void); + +#if !HAVE_READLINE +char *readline(const char *); +void add_history(const char *); +#endif + +int dir_listing(const char *, int (*)(const struct np_stat *), int); + +void cmd_bell(int, const char **); +void cmd_bye(int, const char **); +void cmd_cd(int, const char **); +void cmd_edit(int, const char **); +void cmd_get(int, const char **); +void cmd_hexdump(int, const char **); +void cmd_lcd(int, const char **); +void cmd_lpwd(int, const char **); +void cmd_ls(int, const char **); +void cmd_page(int, const char **); +void cmd_pipe(int, const char **); +void cmd_put(int, const char **); +void cmd_rename(int, const char **); +void cmd_rm(int, const char **); +void cmd_verbose(int, const char **); + +struct cmd { + const char *name; + int cmdtype; + void (*fn)(int, const char **); +}; + +static struct cmd cmds[] = { + {"bell", CMD_BELL, cmd_bell}, + {"bye", CMD_BYE, cmd_bye}, + {"cd", CMD_CD, cmd_cd}, + {"edit", CMD_EDIT, cmd_edit}, + {"get", CMD_GET, cmd_get}, + {"hexdump", CMD_HEXDUMP, cmd_hexdump}, + {"lcd", CMD_LCD, cmd_lcd}, + {"lpwd", CMD_LPWD, cmd_lpwd}, + {"ls", CMD_LS, cmd_ls}, + {"page", CMD_PAGE, cmd_page}, + {"pipe", CMD_PIPE, cmd_pipe}, + {"put", CMD_PUT, cmd_put}, + {"quit", CMD_BYE, cmd_bye}, /* alias */ + {"rename", CMD_RENAME, cmd_rename}, + {"rm", CMD_RM, cmd_rm}, + {"verbose", CMD_VERBOSE, cmd_verbose}, +}; blob - /dev/null blob + bb79125082d73d0e0582a582c068c5b6fd716a7e (mode 644) --- /dev/null +++ kamiftp/rl.c @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2022 Omar Polo + * + * 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. + */ + +#include "compat.h" + +#if !HAVE_READLINE + +#include +#include + +char * +readline(const char *prompt) +{ + char *ch, *line = NULL; + size_t linesize = 0; + ssize_t linelen; + + printf("%s", prompt); + fflush(stdout); + + linelen = getline(&line, &linesize, stdin); + if (linelen == -1) + return NULL; + + if ((ch = strchr(line, '\n')) != NULL) + *ch = '\0'; + return line; +} + +void +add_history(const char *line) +{ + return; +} + +void +compl_setup(void) +{ + return; +} + +#else /* HAVE_READLINE */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "kami.h" +#include "kamiftp.h" + +struct compl_state { + size_t size; + size_t len; + char **entries; +}; + +static struct compl_state compl_state; +static char compl_prfx[PATH_MAX]; + +static void +compl_state_reset(void) +{ + size_t i; + + for (i = 0; i < compl_state.len; ++i) + free(compl_state.entries[i]); + free(compl_state.entries); + + compl_state.len = 0; + compl_state.size = 16; + if ((compl_state.entries = calloc(16, sizeof(char *))) == NULL) + compl_state.size = 0; +} + +static int +compl_add_entry(const struct np_stat *st) +{ + const char *sufx = ""; + char *dup; + int r; + + if (compl_state.len == compl_state.size) { + size_t newsz = compl_state.size * 1.5; + void *t; + + t = recallocarray(compl_state.entries, compl_state.size, + newsz, sizeof(char *)); + if (t == NULL) + return -1; + compl_state.entries = t; + compl_state.size = newsz; + } + + if (st->qid.type & QTDIR) + sufx = "/"; + + if (asprintf(&dup, "%s%s%s", compl_prfx, st->name, sufx) == -1) + return -1; + compl_state.entries[compl_state.len++] = dup; + return 0; +} + +static void +cleanword(char *buf, int brkspc) +{ + char *cmd; + int escape, quote; + + while (brkspc && isspace((unsigned char)*buf)) + memmove(buf, buf + 1, strlen(buf)); + + escape = quote = 0; + for (cmd = buf; *cmd != '\0'; ++cmd) { + if (escape) { + escape = 0; + continue; + } + if (*cmd == '\\') + goto skip; + if (*cmd == quote) { + quote = 0; + goto skip; + } + if (*cmd == '\'' || *cmd == '"') { + quote = *cmd; + goto skip; + } + if (quote) + continue; + if (brkspc && isspace((unsigned char)*cmd)) + break; + continue; + + skip: + memmove(cmd, cmd + 1, strlen(cmd)); + cmd--; + } + *cmd = '\0'; +} + +static int +tellcmd(char *buf) +{ + size_t i; + + cleanword(buf, 1); + for (i = 0; i < nitems(cmds); ++i) { + if (!strcmp(cmds[i].name, buf)) + return cmds[i].cmdtype; + } + + return CMD_UNKNOWN; +} + +static int +tell_argno(const char *cmd, int *cmdtype) +{ + char cmd0[64]; /* plenty of space */ + const char *start = cmd; + int escape, quote; + int argno = 0; + + *cmdtype = CMD_UNKNOWN; + + /* find which argument needs to be completed */ + while (*cmd) { + while (isspace((unsigned char)*cmd)) + cmd++; + if (*cmd == '\0') + break; + + escape = quote = 0; + for (; *cmd; ++cmd) { + if (escape) { + escape = 0; + continue; + } + if (*cmd == '\\') { + escape = 1; + continue; + } + if (*cmd == quote) { + quote = 0; + continue; + } + if (*cmd == '\'' || *cmd == '\"') { + quote = *cmd; + continue; + } + if (quote) + continue; + if (isspace((unsigned char)*cmd)) + break; + } + if (isspace((unsigned char)*cmd)) + argno++; + + if (argno == 1 && strlcpy(cmd0, start, sizeof(cmd0)) < + sizeof(cmd0)) + *cmdtype = tellcmd(cmd0); + } + + return argno; +} + +static char * +ftp_cmdname_generator(const char *text, int state) +{ + static size_t i, len; + struct cmd *cmd; + + if (state == 0) { + i = 0; + len = strlen(text); + } + + while (i < nitems(cmds)) { + cmd = &cmds[i++]; + if (strncmp(text, cmd->name, len) == 0) + return strdup(cmd->name); + } + + return NULL; +} + +static char * +ftp_bool_generator(const char *text, int state) +{ + static const char *toks[] = { "on", "off" }; + static size_t i, len; + const char *tok; + + if (state == 0) { + i = 0; + len = strlen(text); + } + + while ((tok = toks[i++]) != NULL) { + if (strncmp(text, tok, len) == 0) + return strdup(tok); + } + return NULL; +} + +static char * +ftp_dirent_generator(const char *text, int state) +{ + static size_t i, len; + const char *entry; + + if (state == 0) { + i = 0; + len = strlen(text); + } + + while (i < compl_state.len) { + entry = compl_state.entries[i++]; + if (strncmp(text, entry, len) == 0) + return strdup(entry); + } + return NULL; +} + +static char ** +ftp_remote_files(const char *text, int start, int end) +{ + const char *dir; + char t[PATH_MAX]; + char *s, *e; + + strlcpy(t, text, sizeof(t)); + cleanword(t, 0); + + if (!strcmp(t, "..")) { + char **cs; + if ((cs = calloc(2, sizeof(*cs))) == NULL) + return NULL; + cs[0] = strdup("../"); + return cs; + } + + s = t; + if (!strncmp(s, "./", 2)) { + s++; + while (*s == '/') + s++; + } + + if ((e = strrchr(s, '/')) != NULL) + e[1] = '\0'; + dir = t; + + if (!strcmp(dir, ".")) + strlcpy(compl_prfx, "", sizeof(compl_prfx)); + else + strlcpy(compl_prfx, dir, sizeof(compl_prfx)); + + compl_state_reset(); + if (dir_listing(dir, compl_add_entry, 0) == -1) + return NULL; + return rl_completion_matches(text, ftp_dirent_generator); +} + +static char ** +ftp_completion(const char *text, int start, int end) +{ + int argno, cmdtype; + char *line; + + /* don't fall back on the default completion system by default */ + rl_attempted_completion_over = 1; + + if ((line = rl_copy_text(0, start)) == NULL) + return NULL; + + argno = tell_argno(line, &cmdtype); + free(line); + if (argno == 0) + return rl_completion_matches(text, ftp_cmdname_generator); + + switch (cmdtype) { + case CMD_BELL: + case CMD_HEXDUMP: + case CMD_VERBOSE: + if (argno != 1) + return NULL; + return rl_completion_matches(text, ftp_bool_generator); + + case CMD_BYE: + case CMD_LPWD: + /* no args */ + return NULL; + + case CMD_CD: + case CMD_EDIT: + case CMD_LS: + case CMD_PAGE: + if (argno != 1) + return NULL; + /* fallthrough */ + case CMD_RM: + return ftp_remote_files(text, start, end); + + case CMD_GET: + if (argno > 2) + return NULL; + if (argno == 2) + return ftp_remote_files(text, start, end); + /* try local */ + rl_attempted_completion_over = 0; + return NULL; + + case CMD_LCD: + if (argno != 1) + return NULL; + /* try local */ + rl_attempted_completion_over = 0; + return NULL; + + case CMD_PIPE: + if (argno > 2) + return NULL; + if (argno == 1) + return ftp_remote_files(text, start, end); + /* try local */ + rl_attempted_completion_over = 0; + return NULL; + + case CMD_PUT: + if (argno > 2) + return NULL; + if (argno == 1) { + /* try local */ + rl_attempted_completion_over = 0; + return NULL; + } + return ftp_remote_files(text, start, end); + + case CMD_RENAME: + if (argno > 2) + return NULL; + return ftp_remote_files(text, start, end); + } + + return NULL; +} + +static int +ftp_quoted(char *line, int index) +{ + if (index > 0 && line[index - 1] == '\\') + return !ftp_quoted(line, index - 1); + return 0; +} + +void +compl_setup(void) +{ + rl_attempted_completion_function = ftp_completion; + rl_completer_word_break_characters = "\t "; + rl_completer_quote_characters = "\"'"; + rl_char_is_quoted_p = ftp_quoted; +} + +#endif