diff --git a/src/nnn.c b/src/nnn.c index f8c03cf2..a66fa441 100644 --- a/src/nnn.c +++ b/src/nnn.c @@ -109,1461 +109,28 @@ #define alloca(size) __builtin_alloca(size) #endif -#include "nnn.h" #include "dbg.h" +#include "nnn.h" /* Macro definitions */ #define VERSION "3.3" #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn" #define SESSIONS_VERSION 1 -#ifndef S_BLKSIZE -#define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */ -#endif - -/* - * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a - * flexible array on Illumos. Use somewhat accomodating fallback values. - */ -#ifndef NAME_MAX -#define NAME_MAX 255 -#endif - -#ifndef PATH_MAX -#define PATH_MAX 4096 -#endif - -#define _ABSSUB(N, M) (((N) <= (M)) ? ((M) - (N)) : ((N) - (M))) -#define DOUBLECLICK_INTERVAL_NS (400000000) -#define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */ -#define ELEMENTS(x) (sizeof(x) / sizeof(*(x))) -#undef MIN -#define MIN(x, y) ((x) < (y) ? (x) : (y)) -#undef MAX -#define MAX(x, y) ((x) > (y) ? (x) : (y)) -#define ISODD(x) ((x) & 1) -#define ISBLANK(x) ((x) == ' ' || (x) == '\t') -#define TOUPPER(ch) (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch)) -#define CMD_LEN_MAX (PATH_MAX + ((NAME_MAX + 1) << 1)) -#define READLINE_MAX 256 -#define FILTER '/' -#define RFILTER '\\' -#define CASE ':' -#define MSGWAIT '$' -#define SELECT ' ' -#define REGEX_MAX 48 -#define ENTRY_INCR 64 /* Number of dir 'entry' structures to allocate per shot */ -#define NAMEBUF_INCR 0x800 /* 64 dir entries at once, avg. 32 chars per filename = 64*32B = 2KB */ -#define DESCRIPTOR_LEN 32 -#define _ALIGNMENT 0x10 /* 16-byte alignment */ -#define _ALIGNMENT_MASK 0xF -#define TMP_LEN_MAX 64 -#define DOT_FILTER_LEN 7 -#define ASCII_MAX 128 -#define EXEC_ARGS_MAX 8 -#define LIST_FILES_MAX (1 << 16) -#define SCROLLOFF 3 - -#ifndef CTX8 -#define CTX_MAX 4 -#else -#define CTX_MAX 8 -#endif - -#define MIN_DISPLAY_COLS ((CTX_MAX * 2) + 2) /* Two chars for [ and ] */ -#define LONG_SIZE sizeof(ulong) -#define ARCHIVE_CMD_LEN 16 -#define BLK_SHIFT_512 9 - -/* Detect hardlinks in du */ -#define HASH_BITS (0xFFFFFF) -#define HASH_OCTETS (HASH_BITS >> 6) /* 2^6 = 64 */ - -/* Entry flags */ -#define DIR_OR_LINK_TO_DIR 0x01 -#define HARD_LINK 0x02 -#define SYM_ORPHAN 0x04 -#define FILE_MISSING 0x08 -#define FILE_SELECTED 0x10 - -/* Macros to define process spawn behaviour as flags */ -#define F_NONE 0x00 /* no flag set */ -#define F_MULTI 0x01 /* first arg can be combination of args; to be used with F_NORMAL */ -#define F_NOWAIT 0x02 /* don't wait for child process (e.g. file manager) */ -#define F_NOTRACE 0x04 /* suppress stdout and strerr (no traces) */ -#define F_NORMAL 0x08 /* spawn child process in non-curses regular CLI mode */ -#define F_CONFIRM 0x10 /* run command - show results before exit (must have F_NORMAL) */ -#define F_CHKRTN 0x20 /* wait for user prompt if cmd returns failure status */ -#define F_CLI (F_NORMAL | F_MULTI) -#define F_SILENT (F_CLI | F_NOTRACE) - -/* Version compare macros */ -/* - * states: S_N: normal, S_I: comparing integral part, S_F: comparing - * fractional parts, S_Z: idem but with leading Zeroes only - */ -#define S_N 0x0 -#define S_I 0x3 -#define S_F 0x6 -#define S_Z 0x9 - -/* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */ -#define VCMP 2 -#define VLEN 3 - -/* Volume info */ -#define FREE 0 -#define CAPACITY 1 - -/* TYPE DEFINITIONS */ -typedef unsigned long ulong; -typedef unsigned int uint; -typedef unsigned char uchar; -typedef unsigned short ushort; -typedef long long ll; -typedef unsigned long long ull; - -/* STRUCTURES */ - -/* Directory entry */ -typedef struct entry { - char *name; - time_t t; - off_t size; - blkcnt_t blocks; /* number of 512B blocks allocated */ - mode_t mode; - ushort nlen; /* Length of file name */ - uchar flags; /* Flags specific to the file */ -} *pEntry; - -/* Key-value pairs from env */ -typedef struct { - int key; - int off; -} kv; - -typedef struct { -#ifdef PCRE - const pcre *pcrex; -#else - const regex_t *regex; -#endif - const char *str; -} fltrexp_t; - -/* - * Settings - * NOTE: update default values if changing order - */ -typedef struct { - uint filtermode : 1; /* Set to enter filter mode */ - uint timeorder : 1; /* Set to sort by time */ - uint sizeorder : 1; /* Set to sort by file size */ - uint apparentsz : 1; /* Set to sort by apparent size (disk usage) */ - uint blkorder : 1; /* Set to sort by blocks used (disk usage) */ - uint extnorder : 1; /* Order by extension */ - uint showhidden : 1; /* Set to show hidden files */ - uint reserved0 : 1; - uint showdetail : 1; /* Clear to show lesser file info */ - uint ctxactive : 1; /* Context active or not */ - uint reverse : 1; /* Reverse sort */ - uint version : 1; /* Version sort */ - uint reserved1 : 1; - /* The following settings are global */ - uint curctx : 3; /* Current context number */ - uint prefersel : 1; /* Prefer selection over current, if exists */ - uint reserved2 : 1; - uint nonavopen : 1; /* Open file on right arrow or `l` */ - uint autoselect : 1; /* Auto-select dir in type-to-nav mode */ - uint cursormode : 1; /* Move hardware cursor with selection */ - uint useeditor : 1; /* Use VISUAL to open text files */ - uint reserved3 : 3; - uint regex : 1; /* Use regex filters */ - uint x11 : 1; /* Copy to system clipboard and show notis */ - uint timetype : 2; /* Time sort type (0: access, 1: change, 2: modification) */ - uint cliopener : 1; /* All-CLI app opener */ - uint waitedit : 1; /* For ops that can't be detached, used EDITOR */ - uint rollover : 1; /* Roll over at edges */ -} settings; - -/* Non-persistent program-internal states */ -typedef struct { - uint pluginit : 1; /* Plugin framework initialized */ - uint interrupt : 1; /* Program received an interrupt */ - uint rangesel : 1; /* Range selection on */ - uint move : 1; /* Move operation */ - uint autonext : 1; /* Auto-proceed on open */ - uint fortune : 1; /* Show fortune messages in help */ - uint trash : 1; /* Use trash to delete files */ - uint forcequit : 1; /* Do not prompt on quit */ - uint autofifo : 1; /* Auto-create NNN_FIFO */ - uint initfile : 1; /* Positional arg is a file */ - uint dircolor : 1; /* Current status of dir color */ - uint picker : 1; /* Write selection to user-specified file */ - uint pickraw : 1; /* Write selection to sdtout before exit */ - uint runplugin : 1; /* Choose plugin mode */ - uint runctx : 2; /* The context in which plugin is to be run */ - uint selmode : 1; /* Set when selecting files */ - uint oldcolor : 1; /* Show dirs in context colors */ - uint reserved : 14; -} runstate; - -/* Contexts or workspaces */ -typedef struct { - char c_path[PATH_MAX]; /* Current dir */ - char c_last[PATH_MAX]; /* Last visited dir */ - char c_name[NAME_MAX + 1]; /* Current file name */ - char c_fltr[REGEX_MAX]; /* Current filter */ - settings c_cfg; /* Current configuration */ - uint color; /* Color code for directories */ -} context; - -typedef struct { - size_t ver; - size_t pathln[CTX_MAX]; - size_t lastln[CTX_MAX]; - size_t nameln[CTX_MAX]; - size_t fltrln[CTX_MAX]; -} session_header_t; - -/* GLOBALS */ - -/* Configuration, contexts */ -static settings cfg = { - 0, /* filtermode */ - 0, /* timeorder */ - 0, /* sizeorder */ - 0, /* apparentsz */ - 0, /* blkorder */ - 0, /* extnorder */ - 0, /* showhidden */ - 0, /* reserved0 */ - 0, /* showdetail */ - 1, /* ctxactive */ - 0, /* reverse */ - 0, /* version */ - 0, /* reserved1 */ - 0, /* curctx */ - 0, /* prefersel */ - 0, /* reserved2 */ - 0, /* nonavopen */ - 1, /* autoselect */ - 0, /* cursormode */ - 0, /* useeditor */ - 0, /* reserved3 */ - 0, /* regex */ - 0, /* x11 */ - 2, /* timetype (T_MOD) */ - 0, /* cliopener */ - 0, /* waitedit */ - 1, /* rollover */ -}; - -static context g_ctx[CTX_MAX] __attribute__ ((aligned)); - -static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR, scroll_lines = 1; -static int nselected; -#ifndef NOFIFO -static int fifofd = -1; -#endif -static uint idletimeout, selbufpos, lastappendpos, selbuflen; -static ushort xlines, xcols; -static ushort idle; -static uchar maxbm, maxplug; -static char *bmstr; -static char *pluginstr; -static char *opener; -static char *editor; -static char *enveditor; -static char *pager; -static char *shell; -static char *home; -static char *initpath; -static char *cfgpath; -static char *selpath; -static char *listpath; -static char *listroot; -static char *plgpath; -static char *pnamebuf, *pselbuf; -static char *mark; -#ifndef NOFIFO -static char *fifopath; -#endif -static ull *ihashbmp; -static struct entry *pdents; -static blkcnt_t ent_blocks; -static blkcnt_t dir_blocks; -static ulong num_files; -static kv *bookmark; -static kv *plug; -static uchar tmpfplen; -static uchar blk_shift = BLK_SHIFT_512; -#ifndef NOMOUSE -static int middle_click_key; -#endif -#ifdef PCRE -static pcre *archive_pcre; -#else -static regex_t archive_re; -#endif - -/* Retain old signal handlers */ -static struct sigaction oldsighup; -static struct sigaction oldsigtstp; - -/* For use in functions which are isolated and don't return the buffer */ -static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned)); - -/* Buffer to store tmp file path to show selection, file stats and help */ -static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned)); - -/* Buffer to store plugins control pipe location */ -static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned)); - -/* Non-persistent runtime states */ -static runstate g_state; - -/* Options to identify file mime */ -#if defined(__APPLE__) -#define FILE_MIME_OPTS "-bIL" -#elif !defined(__sun) /* no mime option for 'file' */ -#define FILE_MIME_OPTS "-biL" -#endif - -/* Macros for utilities */ -#define UTIL_OPENER 0 -#define UTIL_ATOOL 1 -#define UTIL_BSDTAR 2 -#define UTIL_UNZIP 3 -#define UTIL_TAR 4 -#define UTIL_LOCKER 5 -#define UTIL_LAUNCH 6 -#define UTIL_SH_EXEC 7 -#define UTIL_BASH 8 -#define UTIL_ARCHIVEMOUNT 9 -#define UTIL_SSHFS 10 -#define UTIL_RCLONE 11 -#define UTIL_VI 12 -#define UTIL_LESS 13 -#define UTIL_SH 14 -#define UTIL_FZF 15 -#define UTIL_NTFY 16 -#define UTIL_CBCP 17 -#define UTIL_NMV 18 - -/* Utilities to open files, run actions */ -static char * const utils[] = { -#ifdef __APPLE__ - "/usr/bin/open", -#elif defined __CYGWIN__ - "cygstart", -#elif defined __HAIKU__ - "open", -#else - "xdg-open", -#endif - "atool", - "bsdtar", - "unzip", - "tar", -#ifdef __APPLE__ - "bashlock", -#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) - "lock", -#elif defined __HAIKU__ - "peaclock", -#else - "vlock", -#endif - "launch", - "sh -c", - "bash", - "archivemount", - "sshfs", - "rclone", - "vi", - "less", - "sh", - "fzf", - ".ntfy", - ".cbcp", - ".nmv", -}; - -/* Common strings */ -#define MSG_NO_TRAVERSAL 0 -#define MSG_INVALID_KEY 1 -#define STR_TMPFILE 2 -#define MSG_0_SELECTED 3 -#define MSG_UTIL_MISSING 4 -#define MSG_FAILED 5 -#define MSG_SSN_NAME 6 -#define MSG_CP_MV_AS 7 -#define MSG_CUR_SEL_OPTS 8 -#define MSG_FORCE_RM 9 -#define MSG_LIMIT 10 -#define MSG_NEW_OPTS 11 -#define MSG_CLI_MODE 12 -#define MSG_OVERWRITE 13 -#define MSG_SSN_OPTS 14 -#define MSG_QUIT_ALL 15 -#define MSG_HOSTNAME 16 -#define MSG_ARCHIVE_NAME 17 -#define MSG_OPEN_WITH 18 -#define MSG_REL_PATH 19 -#define MSG_LINK_PREFIX 20 -#define MSG_COPY_NAME 21 -#define MSG_CONTINUE 22 -#define MSG_SEL_MISSING 23 -#define MSG_ACCESS 24 -#define MSG_EMPTY_FILE 25 -#define MSG_UNSUPPORTED 26 -#define MSG_NOT_SET 27 -#define MSG_EXISTS 28 -#define MSG_FEW_COLUMNS 29 -#define MSG_REMOTE_OPTS 30 -#define MSG_RCLONE_DELAY 31 -#define MSG_APP_NAME 32 -#define MSG_ARCHIVE_OPTS 33 -#define MSG_PLUGIN_KEYS 34 -#define MSG_BOOKMARK_KEYS 35 -#define MSG_INVALID_REG 36 -#define MSG_ORDER 37 -#define MSG_LAZY 38 -#define MSG_FIRST 39 -#define MSG_RM_TMP 40 -#define MSG_NOCHNAGE 41 -#define MSG_CANCEL 42 -#define MSG_0_ENTRIES 43 -#ifndef DIR_LIMITED_SELECTION -#define MSG_DIR_CHANGED 44 /* Must be the last entry */ -#endif - -static const char * const messages[] = { - "no traversal", - "invalid key", - "/.nnnXXXXXX", - "0 selected", - "missing util", - "failed!", - "session name: ", - "'c'p / 'm'v as?", - "'c'urrent / 's'el?", - "rm -rf %s file%s? [Esc cancels]", - "limit exceeded", - "'f'ile / 'd'ir / 's'ym / 'h'ard?", - "'c'li / 'g'ui?", - "overwrite?", - "'s'ave / 'l'oad / 'r'estore?", - "Quit all contexts?", - "remote name ('-' for hovered): ", - "archive name: ", - "open with: ", - "relative path: ", - "link prefix [@ for none]: ", - "copy name: ", - "\n'Enter' to continue", - "open failed", - "dir inaccessible", - "empty: edit/open with", - "unknown", - "not set", - "entry exists", - "too few columns!", - "'s'shfs / 'r'clone?", - "refresh if slow", - "app name: ", - "'d'efault / e'x'tract / 'l'ist / 'm'ount?", - "plugin keys:", - "bookmark keys:", - "invalid regex", - "'a'u / 'd'u / 'e'xtn / 'r'ev / 's'ize / 't'ime / 'v'er / 'c'lear?", - "unmount failed! try lazy?", - "first file (\')/char?", - "remove tmp file?", - "unchanged", - "cancelled", - "0 entries", -#ifndef DIR_LIMITED_SELECTION - "dir changed, range sel off", /* Must be the last entry */ -#endif -}; - -/* Supported configuration environment variables */ -#define NNN_OPTS 0 -#define NNN_BMS 1 -#define NNN_PLUG 2 -#define NNN_OPENER 3 -#define NNN_COLORS 4 -#define NNNLVL 5 -#define NNN_PIPE 6 -#define NNN_MCLICK 7 -#define NNN_SEL 8 -#define NNN_ARCHIVE 9 /* strings end here */ -#define NNN_TRASH 10 /* flags begin here */ - -static const char * const env_cfg[] = { - "NNN_OPTS", - "NNN_BMS", - "NNN_PLUG", - "NNN_OPENER", - "NNN_COLORS", - "NNNLVL", - "NNN_PIPE", - "NNN_MCLICK", - "NNN_SEL", - "NNN_ARCHIVE", - "NNN_TRASH", -}; - -/* Required environment variables */ -#define ENV_SHELL 0 -#define ENV_VISUAL 1 -#define ENV_EDITOR 2 -#define ENV_PAGER 3 -#define ENV_NCUR 4 - -static const char * const envs[] = { - "SHELL", - "VISUAL", - "EDITOR", - "PAGER", - "nnn", -}; - -/* Time type used */ -#define T_ACCESS 0 -#define T_CHANGE 1 -#define T_MOD 2 - -#ifdef __linux__ -static char cp[] = "cp -iRp"; -static char mv[] = "mv -i"; -#else -static char cp[] = "cp -iRp"; -static char mv[] = "mv -i"; -#endif - -/* Tokens used for path creation */ -#define TOK_SSN 0 -#define TOK_MNT 1 -#define TOK_PLG 2 - -static const char * const toks[] = { - "sessions", - "mounts", - "plugins", /* must be the last entry */ -}; - -/* Patterns */ -#define P_CPMVFMT 0 -#define P_CPMVRNM 1 -#define P_ARCHIVE 2 -#define P_REPLACE 3 - -static const char * const patterns[] = { - "sed -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s", - "sed 's|^\\([^#/][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' " - "%s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'", - "\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$", - "sed -i 's|^%s\\(.*\\)$|%s\\1|' %s", -}; - -/* Colors */ -#define C_BLK (CTX_MAX + 1) /* Block device: DarkSeaGreen1 */ -#define C_CHR (C_BLK + 1) /* Character device: Yellow1 */ -#define C_DIR (C_CHR + 1) /* Directory: DeepSkyBlue1 */ -#define C_EXE (C_DIR + 1) /* Executable file: Green1 */ -#define C_FIL (C_EXE + 1) /* Regular file: Normal */ -#define C_HRD (C_FIL + 1) /* Hard link: Plum4 */ -#define C_LNK (C_HRD + 1) /* Symbolic link: Cyan1 */ -#define C_MIS (C_LNK + 1) /* Missing file OR file details: Grey62 */ -#define C_ORP (C_MIS + 1) /* Orphaned symlink: DeepPink1 */ -#define C_PIP (C_ORP + 1) /* Named pipe (FIFO): Orange1 */ -#define C_SOC (C_PIP + 1) /* Socket: MediumOrchid1 */ -#define C_UND (C_SOC + 1) /* Unknown OR 0B regular/exe file: Red1 */ - -static char gcolors[] = "c1e2272e006033f7c6d6abc4"; -static uint fcolors[C_UND + 1] = {0}; - -/* Event handling */ -#ifdef LINUX_INOTIFY -#define NUM_EVENT_SLOTS 32 /* Make room for 32 events */ -#define EVENT_SIZE (sizeof(struct inotify_event)) -#define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS) -static int inotify_fd, inotify_wd = -1; -static uint INOTIFY_MASK = /* IN_ATTRIB | */ IN_CREATE | IN_DELETE | IN_DELETE_SELF - | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO; -#elif defined(BSD_KQUEUE) -#define NUM_EVENT_SLOTS 1 -#define NUM_EVENT_FDS 1 -static int kq, event_fd = -1; -static struct kevent events_to_monitor[NUM_EVENT_FDS]; -static uint KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK - | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE; -static struct timespec gtimeout; -#elif defined(HAIKU_NM) -static bool haiku_nm_active = FALSE; -static haiku_nm_h haiku_hnd; -#endif - -/* Function macros */ -#define tolastln() move(xlines - 1, 0) -#define tocursor() move(cur + 2, 0) -#define exitcurses() endwin() -#define printwarn(presel) printwait(strerror(errno), presel) -#define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/') -#define copycurname() xstrsncpy(lastname, pdents[cur].name, NAME_MAX + 1) -#define settimeout() timeout(1000) -#define cleartimeout() timeout(-1) -#define errexit() printerr(__LINE__) -#define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (watch = TRUE)) -#define filterset() (g_ctx[cfg.curctx].c_fltr[1]) -/* We don't care about the return value from strcmp() */ -#define xstrcmp(a, b) (*(a) != *(b) ? -1 : strcmp((a), (b))) -/* A faster version of xisdigit */ -#define xisdigit(c) ((unsigned int) (c) - '0' <= 9) -#define xerror() perror(xitoa(__LINE__)) - -#ifdef __GNUC__ -#define UNUSED(x) UNUSED_##x __attribute__((__unused__)) -#else -#define UNUSED(x) UNUSED_##x -#endif /* __GNUC__ */ - /* Forward declarations */ static void redraw(char *path); -static int spawn(char *file, char *arg1, char *arg2, uchar flag); -static int (*nftw_fn)(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); static void move_cursor(int target, int ignore_scrolloff); static char *load_input(int fd, const char *path); static int set_sort_flags(int r); +static int (*nftw_fn)(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); -/* Functions */ - -static void sigint_handler(int UNUSED(sig)) -{ - g_state.interrupt = 1; -} - -static void clean_exit_sighandler(int UNUSED(sig)) -{ - exitcurses(); - /* This triggers cleanup() thanks to atexit() */ - exit(EXIT_SUCCESS); -} - -static char *xitoa(uint val) -{ - static char ascbuf[32] = {0}; - int i = 30; - uint rem; - - if (!val) - return "0"; - - while (val && i) { - rem = val / 10; - ascbuf[i] = '0' + (val - (rem * 10)); - val = rem; - --i; - } - - return &ascbuf[++i]; -} - -/* Return the integer value of a char representing HEX */ -static uchar xchartohex(uchar c) -{ - if (xisdigit(c)) - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return c; -} - -/* - * Source: https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/asm/bitops.h - */ -static bool test_set_bit(uint nr) -{ - nr &= HASH_BITS; - - ull *m = ((ull *)ihashbmp) + (nr >> 6); - - if (*m & (1 << (nr & 63))) - return FALSE; - - *m |= 1 << (nr & 63); - - return TRUE; -} - -#if 0 -static bool test_clear_bit(uint nr) -{ - nr &= HASH_BITS; - - ull *m = ((ull *) ihashbmp) + (nr >> 6); - - if (!(*m & (1 << (nr & 63)))) - return FALSE; - - *m &= ~(1 << (nr & 63)); - return TRUE; -} -#endif - -/* Increase the limit on open file descriptors, if possible */ -static rlim_t max_openfds(void) -{ - struct rlimit rl; - rlim_t limit = getrlimit(RLIMIT_NOFILE, &rl); - - if (!limit) { - limit = rl.rlim_cur; - rl.rlim_cur = rl.rlim_max; - - /* Return ~75% of max possible */ - if (setrlimit(RLIMIT_NOFILE, &rl) == 0) { - limit = rl.rlim_max - (rl.rlim_max >> 2); - /* - * 20K is arbitrary. If the limit is set to max possible - * value, the memory usage increases to more than double. - */ - if (limit > 20480) - limit = 20480; - } - } else - limit = 32; - - return limit; -} - -/* - * Wrapper to realloc() - * Frees current memory if realloc() fails and returns NULL. - * - * As per the docs, the *alloc() family is supposed to be memory aligned: - * Ubuntu: http://manpages.ubuntu.com/manpages/xenial/man3/malloc.3.html - * macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man3/malloc.3.html - */ -static void *xrealloc(void *pcur, size_t len) -{ - void *pmem = realloc(pcur, len); - - if (!pmem) - free(pcur); - - return pmem; -} - -/* - * Just a safe strncpy(3) - * Always null ('\0') terminates if both src and dest are valid pointers. - * Returns the number of bytes copied including terminating null byte. - */ -static size_t xstrsncpy(char *restrict dst, const char *restrict src, size_t n) -{ - char *end = memccpy(dst, src, '\0', n); - - if (!end) { - dst[n - 1] = '\0'; // NOLINT - end = dst + n; /* If we return n here, binary size increases due to auto-inlining */ - } - - return end - dst; -} - -static inline size_t xstrlen(const char *restrict s) -{ -#if !defined(__GLIBC__) - return strlen(s); // NOLINT -#else - return (char *)rawmemchr(s, '\0') - s; // NOLINT -#endif -} - -static char *xstrdup(const char *restrict s) -{ - size_t len = xstrlen(s) + 1; - char *ptr = malloc(len); - - if (ptr) - xstrsncpy(ptr, s, len); - return ptr; -} - -static bool is_suffix(const char *restrict str, const char *restrict suffix) -{ - if (!str || !suffix) - return FALSE; - - size_t lenstr = xstrlen(str); - size_t lensuffix = xstrlen(suffix); - - if (lensuffix > lenstr) - return FALSE; - - return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0); -} - -static bool is_prefix(const char *restrict str, const char *restrict prefix, size_t len) -{ - return !strncmp(str, prefix, len); -} - -/* - * The poor man's implementation of memrchr(3). - * We are only looking for '/' in this program. - * And we are NOT expecting a '/' at the end. - * Ideally 0 < n <= xstrlen(s). - */ -static void *xmemrchr(uchar *restrict s, uchar ch, size_t n) -{ -#if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) - return memrchr(s, ch, n); -#else - - if (!s || !n) - return NULL; - - uchar *ptr = s + n; - - do - if (*--ptr == ch) - return ptr; - while (s != ptr); - - return NULL; -#endif -} - -/* A very simplified implementation, changes path */ -static char *xdirname(char *path) -{ - char *base = xmemrchr((uchar *)path, '/', xstrlen(path)); - - if (base == path) - path[1] = '\0'; - else - *base = '\0'; - - return path; -} - -static char *xbasename(char *path) -{ - char *base = xmemrchr((uchar *)path, '/', xstrlen(path)); // NOLINT - - return base ? base + 1 : path; -} - -static char *xextension(const char *fname, size_t len) -{ - return xmemrchr((uchar *)fname, '.', len); -} +/* FUNCTIONS */ static inline bool getutil(char *util) { return spawn("which", util, NULL, F_NORMAL | F_NOTRACE) == 0; } -/* - * Updates out with "dir/name or "/name" - * Returns the number of bytes copied including the terminating NULL byte - */ -static size_t mkpath(const char *dir, const char *name, char *out) -{ - size_t len; - - /* Handle absolute path */ - if (name[0] == '/') // NOLINT - return xstrsncpy(out, name, PATH_MAX); - - /* Handle root case */ - if (istopdir(dir)) - len = 1; - else - len = xstrsncpy(out, dir, PATH_MAX); - - out[len - 1] = '/'; // NOLINT - return (xstrsncpy(out + len, name, PATH_MAX - len) + len); -} - -/* Assumes both the paths passed are directories */ -static char *common_prefix(const char *path, char *prefix) -{ - const char *x = path, *y = prefix; - char *sep; - - if (!path || !*path || !prefix) - return NULL; - - if (!*prefix) { - xstrsncpy(prefix, path, PATH_MAX); - return prefix; - } - - while (*x && *y && (*x == *y)) - ++x, ++y; - - /* Strings are same */ - if (!*x && !*y) - return prefix; - - /* Path is shorter */ - if (!*x && *y == '/') { - xstrsncpy(prefix, path, y - path); - return prefix; - } - - /* Prefix is shorter */ - if (!*y && *x == '/') - return prefix; - - /* Shorten prefix */ - prefix[y - prefix] = '\0'; - - sep = xmemrchr((uchar *)prefix, '/', y - prefix); - if (sep != prefix) - *sep = '\0'; - else /* Just '/' */ - prefix[1] = '\0'; - - return prefix; -} - -/* - * The library function realpath() resolves symlinks. - * If there's a symlink in file list we want to show the symlink not what it's points to. - */ -static char *abspath(const char *path, const char *cwd) -{ - if (!path || !cwd) - return NULL; - - size_t dst_size = 0, src_size = xstrlen(path), cwd_size = xstrlen(cwd); - size_t len = src_size; - const char *src; - char *dst; - /* - * We need to add 2 chars at the end as relative paths may start with: - * ./ (find .) - * no separator (fd .): this needs an additional char for '/' - */ - char *resolved_path = malloc(src_size + (*path == '/' ? 0 : cwd_size) + 2); - if (!resolved_path) - return NULL; - - /* Turn relative paths into absolute */ - if (path[0] != '/') - dst_size = xstrsncpy(resolved_path, cwd, cwd_size + 1) - 1; - else - resolved_path[0] = '\0'; - - src = path; - dst = resolved_path + dst_size; - for (const char *next = NULL; next != path + src_size;) { - next = memchr(src, '/', len); - if (!next) - next = path + src_size; - - if (next - src == 2 && src[0] == '.' && src[1] == '.') { - if (dst - resolved_path) { - dst = xmemrchr((uchar *)resolved_path, '/', dst - resolved_path); - *dst = '\0'; - } - } else if (next - src == 1 && src[0] == '.') { - /* NOP */ - } else if (next - src) { - *(dst++) = '/'; - xstrsncpy(dst, src, next - src + 1); - dst += next - src; - } - - src = next + 1; - len = src_size - (src - path); - } - - if (*resolved_path == '\0') { - resolved_path[0] = '/'; - resolved_path[1] = '\0'; - } - - return resolved_path; -} - -static int create_tmp_file(void) -{ - xstrsncpy(g_tmpfpath + tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - tmpfplen); - - int fd = mkstemp(g_tmpfpath); - - if (fd == -1) { - DPRINTF_S(strerror(errno)); - } - - return fd; -} - -static void clearinfoln(void) -{ - move(xlines - 2, 0); - clrtoeol(); -} - -#ifdef KEY_RESIZE -/* Clear the old prompt */ -static void clearoldprompt(void) -{ - clearinfoln(); - tolastln(); - addch('\n'); -} -#endif - -/* Messages show up at the bottom */ -static inline void printmsg_nc(const char *msg) -{ - tolastln(); - addstr(msg); - addch('\n'); -} - -static void printmsg(const char *msg) -{ - attron(COLOR_PAIR(cfg.curctx + 1)); - printmsg_nc(msg); - attroff(COLOR_PAIR(cfg.curctx + 1)); -} - -static void printwait(const char *msg, int *presel) -{ - printmsg(msg); - if (presel) { - *presel = MSGWAIT; - if (ndents) - xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1); - } -} - -/* Kill curses and display error before exiting */ -static void printerr(int linenum) -{ - exitcurses(); - perror(xitoa(linenum)); - if (!g_state.picker && selpath) - unlink(selpath); - free(pselbuf); - exit(1); -} - -static inline bool xconfirm(int c) -{ - return (c == 'y' || c == 'Y'); -} - -static int get_input(const char *prompt) -{ - if (prompt) - printmsg(prompt); - cleartimeout(); - - int r = getch(); - -#ifdef KEY_RESIZE - while (r == KEY_RESIZE) { - if (prompt) { - clearoldprompt(); - xlines = LINES; - printmsg(prompt); - } - - r = getch(); - } -#endif - settimeout(); - return r; -} - -static int get_cur_or_sel(void) -{ - if (selbufpos && ndents) { - if (cfg.prefersel) - return 's'; - - int choice = get_input(messages[MSG_CUR_SEL_OPTS]); - - return ((choice == 'c' || choice == 's') ? choice : 0); - } - - if (selbufpos) - return 's'; - - if (ndents) - return 'c'; - - return 0; -} - -static void xdelay(useconds_t delay) -{ - refresh(); - usleep(delay); -} - -static char confirm_force(bool selection) -{ - char str[64]; - - snprintf(str, 64, messages[MSG_FORCE_RM], - (selection ? xitoa(nselected) : "current"), (selection ? "(s)" : "")); - - int r = get_input(str); - - if (r == 27) - return '\0'; /* cancel */ - if (r == 'y' || r == 'Y') - return 'f'; /* forceful */ - return 'i'; /* interactive */ -} - -/* Writes buflen char(s) from buf to a file */ -static void writesel(const char *buf, const size_t buflen) -{ - if (g_state.pickraw || !selpath) - return; - - FILE *fp = fopen(selpath, "w"); - - if (fp) { - if (fwrite(buf, 1, buflen, fp) != buflen) - printwarn(NULL); - fclose(fp); - } else - printwarn(NULL); -} - -static void appendfpath(const char *path, const size_t len) -{ - if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) { - selbuflen += PATH_MAX; - pselbuf = xrealloc(pselbuf, selbuflen); - if (!pselbuf) - errexit(); - } - - selbufpos += xstrsncpy(pselbuf + selbufpos, path, len); -} - -/* Write selected file paths to fd, linefeed separated */ -static size_t seltofile(int fd, uint *pcount) -{ - uint lastpos, count = 0; - char *pbuf = pselbuf; - size_t pos = 0; - ssize_t len, prefixlen = 0, initlen = 0; - - if (pcount) - *pcount = 0; - - if (!selbufpos) - return 0; - - lastpos = selbufpos - 1; - - if (listpath) { - prefixlen = (ssize_t)xstrlen(listroot); - initlen = (ssize_t)xstrlen(listpath); - } - - while (pos <= lastpos) { - DPRINTF_S(pbuf); - len = (ssize_t)xstrlen(pbuf); - - if (!listpath || !is_prefix(pbuf, listpath, initlen)) { - if (write(fd, pbuf, len) != len) - return pos; - } else { - if (write(fd, listroot, prefixlen) != prefixlen) - return pos; - if (write(fd, pbuf + initlen, len - initlen) != (len - initlen)) - return pos; - } - - pos += len; - if (pos <= lastpos) { - if (write(fd, "\n", 1) != 1) - return pos; - pbuf += len + 1; - } - ++pos; - ++count; - } - - if (pcount) - *pcount = count; - - return pos; -} - -/* List selection from selection file (another instance) */ -static bool listselfile(void) -{ - struct stat sb; - - if (stat(selpath, &sb) == -1) - return FALSE; - - /* Nothing selected if file size is 0 */ - if (!sb.st_size) - return FALSE; - - snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < %s", selpath); - spawn(utils[UTIL_SH_EXEC], g_buf, NULL, F_CLI | F_CONFIRM); - - return TRUE; -} - -/* Reset selection indicators */ -static void resetselind(void) -{ - for (int r = 0; r < ndents; ++r) - if (pdents[r].flags & FILE_SELECTED) - pdents[r].flags &= ~FILE_SELECTED; -} - -static void startselection(void) -{ - if (!g_state.selmode) { - g_state.selmode = 1; - nselected = 0; - - if (selbufpos) { - resetselind(); - writesel(NULL, 0); - selbufpos = 0; - } - - lastappendpos = 0; - } -} - -static void updateselbuf(const char *path, char *newpath) -{ - size_t r; - - for (int i = 0; i < ndents; ++i) - if (pdents[i].flags & FILE_SELECTED) { - r = mkpath(path, pdents[i].name, newpath); - appendfpath(newpath, r); - } -} - -/* Finish selection procedure before an operation */ -static void endselection(void) -{ - int fd; - ssize_t count; - char buf[sizeof(patterns[P_REPLACE]) + PATH_MAX + (TMP_LEN_MAX << 1)]; - - if (g_state.selmode) - g_state.selmode = 0; - - if (!listpath || !selbufpos) - return; - - fd = create_tmp_file(); - if (fd == -1) { - DPRINTF_S("couldn't create tmp file"); - return; - } - - seltofile(fd, NULL); - if (close(fd)) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - return; - } - - snprintf(buf, sizeof(buf), patterns[P_REPLACE], listpath, listroot, g_tmpfpath); - spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI); - - fd = open(g_tmpfpath, O_RDONLY); - if (fd == -1) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - if (unlink(g_tmpfpath)) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - } - return; - } - - count = read(fd, pselbuf, selbuflen); - if (count < 0) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - if (close(fd) || unlink(g_tmpfpath)) { - DPRINTF_S(strerror(errno)); - } - return; - } - - if (close(fd) || unlink(g_tmpfpath)) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - return; - } - - selbufpos = count; - pselbuf[--count] = '\0'; - for (--count; count > 0; --count) - if (pselbuf[count] == '\n' && pselbuf[count+1] == '/') - pselbuf[count] = '\0'; - - writesel(pselbuf, selbufpos - 1); -} - -static void clearselection(void) -{ - nselected = 0; - selbufpos = 0; - g_state.selmode = 0; - writesel(NULL, 0); -} - -/* Returns: 1 - success, 0 - none selected, -1 - other failure */ -static int editselection(void) -{ - int ret = -1; - int fd, lines = 0; - ssize_t count; - struct stat sb; - time_t mtime; - - if (!selbufpos) - return listselfile(); - - fd = create_tmp_file(); - if (fd == -1) { - DPRINTF_S("couldn't create tmp file"); - return -1; - } - - seltofile(fd, NULL); - if (close(fd)) { - DPRINTF_S(strerror(errno)); - return -1; - } - - /* Save the last modification time */ - if (stat(g_tmpfpath, &sb)) { - DPRINTF_S(strerror(errno)); - unlink(g_tmpfpath); - return -1; - } - mtime = sb.st_mtime; - - spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, F_CLI); - - fd = open(g_tmpfpath, O_RDONLY); - if (fd == -1) { - DPRINTF_S(strerror(errno)); - unlink(g_tmpfpath); - return -1; - } - - fstat(fd, &sb); - - if (mtime == sb.st_mtime) { - DPRINTF_S("selection is not modified"); - unlink(g_tmpfpath); - return 1; - } - - if (sb.st_size > selbufpos) { - DPRINTF_S("edited buffer larger than previous"); - unlink(g_tmpfpath); - goto emptyedit; - } - - count = read(fd, pselbuf, selbuflen); - if (count < 0) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - if (close(fd) || unlink(g_tmpfpath)) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - } - goto emptyedit; - } - - if (close(fd) || unlink(g_tmpfpath)) { - DPRINTF_S(strerror(errno)); - printwarn(NULL); - goto emptyedit; - } - - if (!count) { - ret = 1; - goto emptyedit; - } - - resetselind(); - selbufpos = count; - /* The last character should be '\n' */ - pselbuf[--count] = '\0'; - for (--count; count > 0; --count) { - /* Replace every '\n' that separates two paths */ - if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') { - ++lines; - pselbuf[count] = '\0'; - } - } - - /* Add a line for the last file */ - ++lines; - - if (lines > nselected) { - DPRINTF_S("files added to selection"); - goto emptyedit; - } - - nselected = lines; - writesel(pselbuf, selbufpos - 1); - - return 1; - -emptyedit: - resetselind(); - clearselection(); - return ret; -} - -static bool selsafe(void) -{ - /* Fail if selection file path not generated */ - if (!selpath) { - printmsg(messages[MSG_SEL_MISSING]); - return FALSE; - } - - /* Fail if selection file path isn't accessible */ - if (access(selpath, R_OK | W_OK) == -1) { - errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL); - return FALSE; - } - - return TRUE; -} - static void export_file_list(void) { if (!ndents) @@ -1720,180 +287,6 @@ static bool initcurses(void *oldmask) return TRUE; } -/* No NULL check here as spawn() guards against it */ -static int parseargs(char *line, char **argv) -{ - int count = 0; - - argv[count++] = line; - - while (*line) { // NOLINT - if (ISBLANK(*line)) { - *line++ = '\0'; - - if (!*line) // NOLINT - return count; - - argv[count++] = line; - if (count == EXEC_ARGS_MAX) - return -1; - } - - ++line; - } - - return count; -} - -static pid_t xfork(uchar flag) -{ - int status; - pid_t p = fork(); - struct sigaction dfl_act = {.sa_handler = SIG_DFL}; - - if (p > 0) { - /* the parent ignores the interrupt, quit and hangup signals */ - sigaction(SIGHUP, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsighup); - sigaction(SIGTSTP, &dfl_act, &oldsigtstp); - } else if (p == 0) { - /* We create a grandchild to detach */ - if (flag & F_NOWAIT) { - p = fork(); - - if (p > 0) - _exit(EXIT_SUCCESS); - else if (p == 0) { - sigaction(SIGHUP, &dfl_act, NULL); - sigaction(SIGINT, &dfl_act, NULL); - sigaction(SIGQUIT, &dfl_act, NULL); - sigaction(SIGTSTP, &dfl_act, NULL); - - setsid(); - return p; - } - - perror("fork"); - _exit(EXIT_FAILURE); - } - - /* so they can be used to stop the child */ - sigaction(SIGHUP, &dfl_act, NULL); - sigaction(SIGINT, &dfl_act, NULL); - sigaction(SIGQUIT, &dfl_act, NULL); - sigaction(SIGTSTP, &dfl_act, NULL); - } - - /* This is the parent waiting for the child to create grandchild*/ - if (flag & F_NOWAIT) - waitpid(p, &status, 0); - - if (p == -1) - perror("fork"); - return p; -} - -static int join(pid_t p, uchar flag) -{ - int status = 0xFFFF; - - if (!(flag & F_NOWAIT)) { - /* wait for the child to exit */ - do { - } while (waitpid(p, &status, 0) == -1); - - if (WIFEXITED(status)) { - status = WEXITSTATUS(status); - DPRINTF_D(status); - } - } - - /* restore parent's signal handling */ - sigaction(SIGHUP, &oldsighup, NULL); - sigaction(SIGTSTP, &oldsigtstp, NULL); - - return status; -} - -/* - * Spawns a child process. Behaviour can be controlled using flag. - * Limited to 2 arguments to a program, flag works on bit set. - */ -static int spawn(char *file, char *arg1, char *arg2, uchar flag) -{ - pid_t pid; - int status = 0, retstatus = 0xFFFF; - char *argv[EXEC_ARGS_MAX] = {0}; - char *cmd = NULL; - - if (!file || !*file) - return retstatus; - - /* Swap args if the first arg is NULL and second isn't */ - if (!arg1 && arg2) { - arg1 = arg2; - arg2 = NULL; - } - - if (flag & F_MULTI) { - size_t len = xstrlen(file) + 1; - - cmd = (char *)malloc(len); - if (!cmd) { - DPRINTF_S("malloc()!"); - return retstatus; - } - - xstrsncpy(cmd, file, len); - status = parseargs(cmd, argv); - if (status == -1 || status > (EXEC_ARGS_MAX - 3)) { /* arg1, arg2 and last NULL */ - free(cmd); - DPRINTF_S("NULL or too many args"); - return retstatus; - } - } else - argv[status++] = file; - - argv[status] = arg1; - argv[++status] = arg2; - - if (flag & F_NORMAL) - exitcurses(); - - pid = xfork(flag); - if (pid == 0) { - /* Suppress stdout and stderr */ - if (flag & F_NOTRACE) { - int fd = open("/dev/null", O_WRONLY, 0200); - - dup2(fd, 1); - dup2(fd, 2); - close(fd); - } - - execvp(*argv, argv); - _exit(EXIT_SUCCESS); - } else { - retstatus = join(pid, flag); - - DPRINTF_D(pid); - - if ((flag & F_CONFIRM) || ((flag & F_CHKRTN) && retstatus)) { - printf("%s", messages[MSG_CONTINUE]); -#ifndef NORL - fflush(stdout); -#endif - while (getchar() != '\n'); - } - - if (flag & F_NORMAL) - refresh(); - - free(cmd); - } - - return retstatus; -} - static void prompt_run(char *cmd, const char *current) { setenv(envs[ENV_NCUR], current, 1); diff --git a/src/nnn.h b/src/nnn.h index 57c8649f..a6e67ec0 100644 --- a/src/nnn.h +++ b/src/nnn.h @@ -32,7 +32,114 @@ #include +#ifndef S_BLKSIZE +#define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */ +#endif + +/* + * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a + * flexible array on Illumos. Use somewhat accomodating fallback values. + */ +#ifndef NAME_MAX +#define NAME_MAX 255 +#endif + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + #define CONTROL(c) ((c) & 0x1f) +#define _ABSSUB(N, M) (((N) <= (M)) ? ((M) - (N)) : ((N) - (M))) +#define DOUBLECLICK_INTERVAL_NS (400000000) +#define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */ +#define ELEMENTS(x) (sizeof(x) / sizeof(*(x))) +#undef MIN +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#undef MAX +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#define ISODD(x) ((x) & 1) +#define ISBLANK(x) ((x) == ' ' || (x) == '\t') +#define TOUPPER(ch) (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch)) +#define CMD_LEN_MAX (PATH_MAX + ((NAME_MAX + 1) << 1)) +#define READLINE_MAX 256 +#define FILTER '/' +#define RFILTER '\\' +#define CASE ':' +#define MSGWAIT '$' +#define SELECT ' ' +#define REGEX_MAX 48 +#define ENTRY_INCR 64 /* Number of dir 'entry' structures to allocate per shot */ +#define NAMEBUF_INCR 0x800 /* 64 dir entries at once, avg. 32 chars per filename = 64*32B = 2KB */ +#define DESCRIPTOR_LEN 32 +#define _ALIGNMENT 0x10 /* 16-byte alignment */ +#define _ALIGNMENT_MASK 0xF +#define TMP_LEN_MAX 64 +#define DOT_FILTER_LEN 7 +#define ASCII_MAX 128 +#define EXEC_ARGS_MAX 8 +#define LIST_FILES_MAX (1 << 16) +#define SCROLLOFF 3 + +#ifndef CTX8 +#define CTX_MAX 4 +#else +#define CTX_MAX 8 +#endif + +#define MIN_DISPLAY_COLS ((CTX_MAX * 2) + 2) /* Two chars for [ and ] */ +#define LONG_SIZE sizeof(ulong) +#define ARCHIVE_CMD_LEN 16 +#define BLK_SHIFT_512 9 + +/* Detect hardlinks in du */ +#define HASH_BITS (0xFFFFFF) +#define HASH_OCTETS (HASH_BITS >> 6) /* 2^6 = 64 */ + +/* Entry flags */ +#define DIR_OR_LINK_TO_DIR 0x01 +#define HARD_LINK 0x02 +#define SYM_ORPHAN 0x04 +#define FILE_MISSING 0x08 +#define FILE_SELECTED 0x10 + +/* Macros to define process spawn behaviour as flags */ +#define F_NONE 0x00 /* no flag set */ +#define F_MULTI 0x01 /* first arg can be combination of args; to be used with F_NORMAL */ +#define F_NOWAIT 0x02 /* don't wait for child process (e.g. file manager) */ +#define F_NOTRACE 0x04 /* suppress stdout and strerr (no traces) */ +#define F_NORMAL 0x08 /* spawn child process in non-curses regular CLI mode */ +#define F_CONFIRM 0x10 /* run command - show results before exit (must have F_NORMAL) */ +#define F_CHKRTN 0x20 /* wait for user prompt if cmd returns failure status */ +#define F_CLI (F_NORMAL | F_MULTI) +#define F_SILENT (F_CLI | F_NOTRACE) + +/* Version compare macros */ +/* + * states: S_N: normal, S_I: comparing integral part, S_F: comparing + * fractional parts, S_Z: idem but with leading Zeroes only + */ +#define S_N 0x0 +#define S_I 0x3 +#define S_F 0x6 +#define S_Z 0x9 + +/* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */ +#define VCMP 2 +#define VLEN 3 + +/* Volume info */ +#define FREE 0 +#define CAPACITY 1 + +/* TYPE DEFINITIONS */ +typedef unsigned long ulong; +typedef unsigned int uint; +typedef unsigned char uchar; +typedef unsigned short ushort; +typedef long long ll; +typedef unsigned long long ull; + +/* STRUCTURES */ /* Supported actions */ enum action { @@ -274,3 +381,1509 @@ static struct key bindings[] = { { KEY_MOUSE, SEL_CLICK }, #endif }; + +/* Directory entry */ +typedef struct entry { + char *name; + time_t t; + off_t size; + blkcnt_t blocks; /* number of 512B blocks allocated */ + mode_t mode; + ushort nlen; /* Length of file name */ + uchar flags; /* Flags specific to the file */ +} *pEntry; + +/* Key-value pairs from env */ +typedef struct { + int key; + int off; +} kv; + +typedef struct { +#ifdef PCRE + const pcre *pcrex; +#else + const regex_t *regex; +#endif + const char *str; +} fltrexp_t; + +/* + * Settings + * NOTE: update default values if changing order + */ +typedef struct { + uint filtermode : 1; /* Set to enter filter mode */ + uint timeorder : 1; /* Set to sort by time */ + uint sizeorder : 1; /* Set to sort by file size */ + uint apparentsz : 1; /* Set to sort by apparent size (disk usage) */ + uint blkorder : 1; /* Set to sort by blocks used (disk usage) */ + uint extnorder : 1; /* Order by extension */ + uint showhidden : 1; /* Set to show hidden files */ + uint reserved0 : 1; + uint showdetail : 1; /* Clear to show lesser file info */ + uint ctxactive : 1; /* Context active or not */ + uint reverse : 1; /* Reverse sort */ + uint version : 1; /* Version sort */ + uint reserved1 : 1; + /* The following settings are global */ + uint curctx : 3; /* Current context number */ + uint prefersel : 1; /* Prefer selection over current, if exists */ + uint reserved2 : 1; + uint nonavopen : 1; /* Open file on right arrow or `l` */ + uint autoselect : 1; /* Auto-select dir in type-to-nav mode */ + uint cursormode : 1; /* Move hardware cursor with selection */ + uint useeditor : 1; /* Use VISUAL to open text files */ + uint reserved3 : 3; + uint regex : 1; /* Use regex filters */ + uint x11 : 1; /* Copy to system clipboard and show notis */ + uint timetype : 2; /* Time sort type (0: access, 1: change, 2: modification) */ + uint cliopener : 1; /* All-CLI app opener */ + uint waitedit : 1; /* For ops that can't be detached, used EDITOR */ + uint rollover : 1; /* Roll over at edges */ +} settings; + +/* Non-persistent program-internal states */ +typedef struct { + uint pluginit : 1; /* Plugin framework initialized */ + uint interrupt : 1; /* Program received an interrupt */ + uint rangesel : 1; /* Range selection on */ + uint move : 1; /* Move operation */ + uint autonext : 1; /* Auto-proceed on open */ + uint fortune : 1; /* Show fortune messages in help */ + uint trash : 1; /* Use trash to delete files */ + uint forcequit : 1; /* Do not prompt on quit */ + uint autofifo : 1; /* Auto-create NNN_FIFO */ + uint initfile : 1; /* Positional arg is a file */ + uint dircolor : 1; /* Current status of dir color */ + uint picker : 1; /* Write selection to user-specified file */ + uint pickraw : 1; /* Write selection to sdtout before exit */ + uint runplugin : 1; /* Choose plugin mode */ + uint runctx : 2; /* The context in which plugin is to be run */ + uint selmode : 1; /* Set when selecting files */ + uint oldcolor : 1; /* Show dirs in context colors */ + uint reserved : 14; +} runstate; + +/* Contexts or workspaces */ +typedef struct { + char c_path[PATH_MAX]; /* Current dir */ + char c_last[PATH_MAX]; /* Last visited dir */ + char c_name[NAME_MAX + 1]; /* Current file name */ + char c_fltr[REGEX_MAX]; /* Current filter */ + settings c_cfg; /* Current configuration */ + uint color; /* Color code for directories */ +} context; + +typedef struct { + size_t ver; + size_t pathln[CTX_MAX]; + size_t lastln[CTX_MAX]; + size_t nameln[CTX_MAX]; + size_t fltrln[CTX_MAX]; +} session_header_t; + +/* GLOBALS */ + +/* Configuration, contexts */ +static settings cfg = { + 0, /* filtermode */ + 0, /* timeorder */ + 0, /* sizeorder */ + 0, /* apparentsz */ + 0, /* blkorder */ + 0, /* extnorder */ + 0, /* showhidden */ + 0, /* reserved0 */ + 0, /* showdetail */ + 1, /* ctxactive */ + 0, /* reverse */ + 0, /* version */ + 0, /* reserved1 */ + 0, /* curctx */ + 0, /* prefersel */ + 0, /* reserved2 */ + 0, /* nonavopen */ + 1, /* autoselect */ + 0, /* cursormode */ + 0, /* useeditor */ + 0, /* reserved3 */ + 0, /* regex */ + 0, /* x11 */ + 2, /* timetype (T_MOD) */ + 0, /* cliopener */ + 0, /* waitedit */ + 1, /* rollover */ +}; + +static context g_ctx[CTX_MAX] __attribute__ ((aligned)); + +static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR, scroll_lines = 1; +static int nselected; +#ifndef NOFIFO +static int fifofd = -1; +#endif +static uint idletimeout, selbufpos, lastappendpos, selbuflen; +static ushort xlines, xcols; +static ushort idle; +static uchar maxbm, maxplug; +static char *bmstr; +static char *pluginstr; +static char *opener; +static char *editor; +static char *enveditor; +static char *pager; +static char *shell; +static char *home; +static char *initpath; +static char *cfgpath; +static char *selpath; +static char *listpath; +static char *listroot; +static char *plgpath; +static char *pnamebuf, *pselbuf; +static char *mark; +#ifndef NOFIFO +static char *fifopath; +#endif +static ull *ihashbmp; +static struct entry *pdents; +static blkcnt_t ent_blocks; +static blkcnt_t dir_blocks; +static ulong num_files; +static kv *bookmark; +static kv *plug; +static uchar tmpfplen; +static uchar blk_shift = BLK_SHIFT_512; +#ifndef NOMOUSE +static int middle_click_key; +#endif +#ifdef PCRE +static pcre *archive_pcre; +#else +static regex_t archive_re; +#endif + +/* Retain old signal handlers */ +static struct sigaction oldsighup; +static struct sigaction oldsigtstp; + +/* For use in functions which are isolated and don't return the buffer */ +static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned)); + +/* Buffer to store tmp file path to show selection, file stats and help */ +static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned)); + +/* Buffer to store plugins control pipe location */ +static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned)); + +/* Non-persistent runtime states */ +static runstate g_state; + +/* Options to identify file mime */ +#if defined(__APPLE__) +#define FILE_MIME_OPTS "-bIL" +#elif !defined(__sun) /* no mime option for 'file' */ +#define FILE_MIME_OPTS "-biL" +#endif + +/* Macros for utilities */ +#define UTIL_OPENER 0 +#define UTIL_ATOOL 1 +#define UTIL_BSDTAR 2 +#define UTIL_UNZIP 3 +#define UTIL_TAR 4 +#define UTIL_LOCKER 5 +#define UTIL_LAUNCH 6 +#define UTIL_SH_EXEC 7 +#define UTIL_BASH 8 +#define UTIL_ARCHIVEMOUNT 9 +#define UTIL_SSHFS 10 +#define UTIL_RCLONE 11 +#define UTIL_VI 12 +#define UTIL_LESS 13 +#define UTIL_SH 14 +#define UTIL_FZF 15 +#define UTIL_NTFY 16 +#define UTIL_CBCP 17 +#define UTIL_NMV 18 + +/* Utilities to open files, run actions */ +static char * const utils[] = { +#ifdef __APPLE__ + "/usr/bin/open", +#elif defined __CYGWIN__ + "cygstart", +#elif defined __HAIKU__ + "open", +#else + "xdg-open", +#endif + "atool", + "bsdtar", + "unzip", + "tar", +#ifdef __APPLE__ + "bashlock", +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + "lock", +#elif defined __HAIKU__ + "peaclock", +#else + "vlock", +#endif + "launch", + "sh -c", + "bash", + "archivemount", + "sshfs", + "rclone", + "vi", + "less", + "sh", + "fzf", + ".ntfy", + ".cbcp", + ".nmv", +}; + +/* Common strings */ +#define MSG_NO_TRAVERSAL 0 +#define MSG_INVALID_KEY 1 +#define STR_TMPFILE 2 +#define MSG_0_SELECTED 3 +#define MSG_UTIL_MISSING 4 +#define MSG_FAILED 5 +#define MSG_SSN_NAME 6 +#define MSG_CP_MV_AS 7 +#define MSG_CUR_SEL_OPTS 8 +#define MSG_FORCE_RM 9 +#define MSG_LIMIT 10 +#define MSG_NEW_OPTS 11 +#define MSG_CLI_MODE 12 +#define MSG_OVERWRITE 13 +#define MSG_SSN_OPTS 14 +#define MSG_QUIT_ALL 15 +#define MSG_HOSTNAME 16 +#define MSG_ARCHIVE_NAME 17 +#define MSG_OPEN_WITH 18 +#define MSG_REL_PATH 19 +#define MSG_LINK_PREFIX 20 +#define MSG_COPY_NAME 21 +#define MSG_CONTINUE 22 +#define MSG_SEL_MISSING 23 +#define MSG_ACCESS 24 +#define MSG_EMPTY_FILE 25 +#define MSG_UNSUPPORTED 26 +#define MSG_NOT_SET 27 +#define MSG_EXISTS 28 +#define MSG_FEW_COLUMNS 29 +#define MSG_REMOTE_OPTS 30 +#define MSG_RCLONE_DELAY 31 +#define MSG_APP_NAME 32 +#define MSG_ARCHIVE_OPTS 33 +#define MSG_PLUGIN_KEYS 34 +#define MSG_BOOKMARK_KEYS 35 +#define MSG_INVALID_REG 36 +#define MSG_ORDER 37 +#define MSG_LAZY 38 +#define MSG_FIRST 39 +#define MSG_RM_TMP 40 +#define MSG_NOCHNAGE 41 +#define MSG_CANCEL 42 +#define MSG_0_ENTRIES 43 +#ifndef DIR_LIMITED_SELECTION +#define MSG_DIR_CHANGED 44 /* Must be the last entry */ +#endif + +static const char * const messages[] = { + "no traversal", + "invalid key", + "/.nnnXXXXXX", + "0 selected", + "missing util", + "failed!", + "session name: ", + "'c'p / 'm'v as?", + "'c'urrent / 's'el?", + "rm -rf %s file%s? [Esc cancels]", + "limit exceeded", + "'f'ile / 'd'ir / 's'ym / 'h'ard?", + "'c'li / 'g'ui?", + "overwrite?", + "'s'ave / 'l'oad / 'r'estore?", + "Quit all contexts?", + "remote name ('-' for hovered): ", + "archive name: ", + "open with: ", + "relative path: ", + "link prefix [@ for none]: ", + "copy name: ", + "\n'Enter' to continue", + "open failed", + "dir inaccessible", + "empty: edit/open with", + "unknown", + "not set", + "entry exists", + "too few columns!", + "'s'shfs / 'r'clone?", + "refresh if slow", + "app name: ", + "'d'efault / e'x'tract / 'l'ist / 'm'ount?", + "plugin keys:", + "bookmark keys:", + "invalid regex", + "'a'u / 'd'u / 'e'xtn / 'r'ev / 's'ize / 't'ime / 'v'er / 'c'lear?", + "unmount failed! try lazy?", + "first file (\')/char?", + "remove tmp file?", + "unchanged", + "cancelled", + "0 entries", +#ifndef DIR_LIMITED_SELECTION + "dir changed, range sel off", /* Must be the last entry */ +#endif +}; + +/* Supported configuration environment variables */ +#define NNN_OPTS 0 +#define NNN_BMS 1 +#define NNN_PLUG 2 +#define NNN_OPENER 3 +#define NNN_COLORS 4 +#define NNNLVL 5 +#define NNN_PIPE 6 +#define NNN_MCLICK 7 +#define NNN_SEL 8 +#define NNN_ARCHIVE 9 /* strings end here */ +#define NNN_TRASH 10 /* flags begin here */ + +static const char * const env_cfg[] = { + "NNN_OPTS", + "NNN_BMS", + "NNN_PLUG", + "NNN_OPENER", + "NNN_COLORS", + "NNNLVL", + "NNN_PIPE", + "NNN_MCLICK", + "NNN_SEL", + "NNN_ARCHIVE", + "NNN_TRASH", +}; + +/* Required environment variables */ +#define ENV_SHELL 0 +#define ENV_VISUAL 1 +#define ENV_EDITOR 2 +#define ENV_PAGER 3 +#define ENV_NCUR 4 + +static const char * const envs[] = { + "SHELL", + "VISUAL", + "EDITOR", + "PAGER", + "nnn", +}; + +/* Time type used */ +#define T_ACCESS 0 +#define T_CHANGE 1 +#define T_MOD 2 + +#ifdef __linux__ +static char cp[] = "cp -iRp"; +static char mv[] = "mv -i"; +#else +static char cp[] = "cp -iRp"; +static char mv[] = "mv -i"; +#endif + +/* Tokens used for path creation */ +#define TOK_SSN 0 +#define TOK_MNT 1 +#define TOK_PLG 2 + +static const char * const toks[] = { + "sessions", + "mounts", + "plugins", /* must be the last entry */ +}; + +/* Patterns */ +#define P_CPMVFMT 0 +#define P_CPMVRNM 1 +#define P_ARCHIVE 2 +#define P_REPLACE 3 + +static const char * const patterns[] = { + "sed -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s", + "sed 's|^\\([^#/][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' " + "%s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'", + "\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$", + "sed -i 's|^%s\\(.*\\)$|%s\\1|' %s", +}; + +/* Colors */ +#define C_BLK (CTX_MAX + 1) /* Block device: DarkSeaGreen1 */ +#define C_CHR (C_BLK + 1) /* Character device: Yellow1 */ +#define C_DIR (C_CHR + 1) /* Directory: DeepSkyBlue1 */ +#define C_EXE (C_DIR + 1) /* Executable file: Green1 */ +#define C_FIL (C_EXE + 1) /* Regular file: Normal */ +#define C_HRD (C_FIL + 1) /* Hard link: Plum4 */ +#define C_LNK (C_HRD + 1) /* Symbolic link: Cyan1 */ +#define C_MIS (C_LNK + 1) /* Missing file OR file details: Grey62 */ +#define C_ORP (C_MIS + 1) /* Orphaned symlink: DeepPink1 */ +#define C_PIP (C_ORP + 1) /* Named pipe (FIFO): Orange1 */ +#define C_SOC (C_PIP + 1) /* Socket: MediumOrchid1 */ +#define C_UND (C_SOC + 1) /* Unknown OR 0B regular/exe file: Red1 */ + +static char gcolors[] = "c1e2272e006033f7c6d6abc4"; +static uint fcolors[C_UND + 1] = {0}; + +/* Event handling */ +#ifdef LINUX_INOTIFY +#define NUM_EVENT_SLOTS 32 /* Make room for 32 events */ +#define EVENT_SIZE (sizeof(struct inotify_event)) +#define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS) +static int inotify_fd, inotify_wd = -1; +static uint INOTIFY_MASK = /* IN_ATTRIB | */ IN_CREATE | IN_DELETE | IN_DELETE_SELF + | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO; +#elif defined(BSD_KQUEUE) +#define NUM_EVENT_SLOTS 1 +#define NUM_EVENT_FDS 1 +static int kq, event_fd = -1; +static struct kevent events_to_monitor[NUM_EVENT_FDS]; +static uint KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK + | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE; +static struct timespec gtimeout; +#elif defined(HAIKU_NM) +static bool haiku_nm_active = FALSE; +static haiku_nm_h haiku_hnd; +#endif + +/* Function macros */ +#define tolastln() move(xlines - 1, 0) +#define tocursor() move(cur + 2, 0) +#define exitcurses() endwin() +#define printwarn(presel) printwait(strerror(errno), presel) +#define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/') +#define copycurname() xstrsncpy(lastname, pdents[cur].name, NAME_MAX + 1) +#define settimeout() timeout(1000) +#define cleartimeout() timeout(-1) +#define errexit() printerr(__LINE__) +#define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (watch = TRUE)) +#define filterset() (g_ctx[cfg.curctx].c_fltr[1]) +/* We don't care about the return value from strcmp() */ +#define xstrcmp(a, b) (*(a) != *(b) ? -1 : strcmp((a), (b))) +/* A faster version of xisdigit */ +#define xisdigit(c) ((unsigned int) (c) - '0' <= 9) +#define xerror() perror(xitoa(__LINE__)) + +#ifdef __GNUC__ +#define UNUSED(x) UNUSED_##x __attribute__((__unused__)) +#else +#define UNUSED(x) UNUSED_##x +#endif /* __GNUC__ */ + +/* HELPER FUNCTIONS */ + +static void sigint_handler(int UNUSED(sig)) +{ + g_state.interrupt = 1; +} + +static void clean_exit_sighandler(int UNUSED(sig)) +{ + exitcurses(); + /* This triggers cleanup() thanks to atexit() */ + exit(EXIT_SUCCESS); +} + +static char *xitoa(uint val) +{ + static char ascbuf[32] = {0}; + int i = 30; + uint rem; + + if (!val) + return "0"; + + while (val && i) { + rem = val / 10; + ascbuf[i] = '0' + (val - (rem * 10)); + val = rem; + --i; + } + + return &ascbuf[++i]; +} + +/* Return the integer value of a char representing HEX */ +static uchar xchartohex(uchar c) +{ + if (xisdigit(c)) + return c - '0'; + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return c; +} + +/* + * Source: https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/asm/bitops.h + */ +static bool test_set_bit(uint nr) +{ + nr &= HASH_BITS; + + ull *m = ((ull *)ihashbmp) + (nr >> 6); + + if (*m & (1 << (nr & 63))) + return FALSE; + + *m |= 1 << (nr & 63); + + return TRUE; +} + +#if 0 +static bool test_clear_bit(uint nr) +{ + nr &= HASH_BITS; + + ull *m = ((ull *) ihashbmp) + (nr >> 6); + + if (!(*m & (1 << (nr & 63)))) + return FALSE; + + *m &= ~(1 << (nr & 63)); + return TRUE; +} +#endif + +/* Increase the limit on open file descriptors, if possible */ +static rlim_t max_openfds(void) +{ + struct rlimit rl; + rlim_t limit = getrlimit(RLIMIT_NOFILE, &rl); + + if (!limit) { + limit = rl.rlim_cur; + rl.rlim_cur = rl.rlim_max; + + /* Return ~75% of max possible */ + if (setrlimit(RLIMIT_NOFILE, &rl) == 0) { + limit = rl.rlim_max - (rl.rlim_max >> 2); + /* + * 20K is arbitrary. If the limit is set to max possible + * value, the memory usage increases to more than double. + */ + if (limit > 20480) + limit = 20480; + } + } else + limit = 32; + + return limit; +} + +/* + * Wrapper to realloc() + * Frees current memory if realloc() fails and returns NULL. + * + * As per the docs, the *alloc() family is supposed to be memory aligned: + * Ubuntu: http://manpages.ubuntu.com/manpages/xenial/man3/malloc.3.html + * macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man3/malloc.3.html + */ +static void *xrealloc(void *pcur, size_t len) +{ + void *pmem = realloc(pcur, len); + + if (!pmem) + free(pcur); + + return pmem; +} + +/* + * Just a safe strncpy(3) + * Always null ('\0') terminates if both src and dest are valid pointers. + * Returns the number of bytes copied including terminating null byte. + */ +static size_t xstrsncpy(char *restrict dst, const char *restrict src, size_t n) +{ + char *end = memccpy(dst, src, '\0', n); + + if (!end) { + dst[n - 1] = '\0'; // NOLINT + end = dst + n; /* If we return n here, binary size increases due to auto-inlining */ + } + + return end - dst; +} + +static inline size_t xstrlen(const char *restrict s) +{ +#if !defined(__GLIBC__) + return strlen(s); // NOLINT +#else + return (char *)rawmemchr(s, '\0') - s; // NOLINT +#endif +} + +static char *xstrdup(const char *restrict s) +{ + size_t len = xstrlen(s) + 1; + char *ptr = malloc(len); + + if (ptr) + xstrsncpy(ptr, s, len); + return ptr; +} + +static bool is_suffix(const char *restrict str, const char *restrict suffix) +{ + if (!str || !suffix) + return FALSE; + + size_t lenstr = xstrlen(str); + size_t lensuffix = xstrlen(suffix); + + if (lensuffix > lenstr) + return FALSE; + + return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0); +} + +static bool is_prefix(const char *restrict str, const char *restrict prefix, size_t len) +{ + return !strncmp(str, prefix, len); +} + +/* + * The poor man's implementation of memrchr(3). + * We are only looking for '/' in this program. + * And we are NOT expecting a '/' at the end. + * Ideally 0 < n <= xstrlen(s). + */ +static void *xmemrchr(uchar *restrict s, uchar ch, size_t n) +{ +#if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + return memrchr(s, ch, n); +#else + + if (!s || !n) + return NULL; + + uchar *ptr = s + n; + + do + if (*--ptr == ch) + return ptr; + while (s != ptr); + + return NULL; +#endif +} + +/* A very simplified implementation, changes path */ +static char *xdirname(char *path) +{ + char *base = xmemrchr((uchar *)path, '/', xstrlen(path)); + + if (base == path) + path[1] = '\0'; + else + *base = '\0'; + + return path; +} + +static char *xbasename(char *path) +{ + char *base = xmemrchr((uchar *)path, '/', xstrlen(path)); // NOLINT + + return base ? base + 1 : path; +} + +static char *xextension(const char *fname, size_t len) +{ + return xmemrchr((uchar *)fname, '.', len); +} + +/* + * Updates out with "dir/name or "/name" + * Returns the number of bytes copied including the terminating NULL byte + */ +static size_t mkpath(const char *dir, const char *name, char *out) +{ + size_t len; + + /* Handle absolute path */ + if (name[0] == '/') // NOLINT + return xstrsncpy(out, name, PATH_MAX); + + /* Handle root case */ + if (istopdir(dir)) + len = 1; + else + len = xstrsncpy(out, dir, PATH_MAX); + + out[len - 1] = '/'; // NOLINT + return (xstrsncpy(out + len, name, PATH_MAX - len) + len); +} + +/* Assumes both the paths passed are directories */ +static char *common_prefix(const char *path, char *prefix) +{ + const char *x = path, *y = prefix; + char *sep; + + if (!path || !*path || !prefix) + return NULL; + + if (!*prefix) { + xstrsncpy(prefix, path, PATH_MAX); + return prefix; + } + + while (*x && *y && (*x == *y)) + ++x, ++y; + + /* Strings are same */ + if (!*x && !*y) + return prefix; + + /* Path is shorter */ + if (!*x && *y == '/') { + xstrsncpy(prefix, path, y - path); + return prefix; + } + + /* Prefix is shorter */ + if (!*y && *x == '/') + return prefix; + + /* Shorten prefix */ + prefix[y - prefix] = '\0'; + + sep = xmemrchr((uchar *)prefix, '/', y - prefix); + if (sep != prefix) + *sep = '\0'; + else /* Just '/' */ + prefix[1] = '\0'; + + return prefix; +} + +/* + * The library function realpath() resolves symlinks. + * If there's a symlink in file list we want to show the symlink not what it's points to. + */ +static char *abspath(const char *path, const char *cwd) +{ + if (!path || !cwd) + return NULL; + + size_t dst_size = 0, src_size = xstrlen(path), cwd_size = xstrlen(cwd); + size_t len = src_size; + const char *src; + char *dst; + /* + * We need to add 2 chars at the end as relative paths may start with: + * ./ (find .) + * no separator (fd .): this needs an additional char for '/' + */ + char *resolved_path = malloc(src_size + (*path == '/' ? 0 : cwd_size) + 2); + if (!resolved_path) + return NULL; + + /* Turn relative paths into absolute */ + if (path[0] != '/') + dst_size = xstrsncpy(resolved_path, cwd, cwd_size + 1) - 1; + else + resolved_path[0] = '\0'; + + src = path; + dst = resolved_path + dst_size; + for (const char *next = NULL; next != path + src_size;) { + next = memchr(src, '/', len); + if (!next) + next = path + src_size; + + if (next - src == 2 && src[0] == '.' && src[1] == '.') { + if (dst - resolved_path) { + dst = xmemrchr((uchar *)resolved_path, '/', dst - resolved_path); + *dst = '\0'; + } + } else if (next - src == 1 && src[0] == '.') { + /* NOP */ + } else if (next - src) { + *(dst++) = '/'; + xstrsncpy(dst, src, next - src + 1); + dst += next - src; + } + + src = next + 1; + len = src_size - (src - path); + } + + if (*resolved_path == '\0') { + resolved_path[0] = '/'; + resolved_path[1] = '\0'; + } + + return resolved_path; +} + +static int create_tmp_file(void) +{ + xstrsncpy(g_tmpfpath + tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - tmpfplen); + + int fd = mkstemp(g_tmpfpath); + + if (fd == -1) { + DPRINTF_S(strerror(errno)); + } + + return fd; +} + +/* PRINT I/O FUNCTIONS */ + +static void clearinfoln(void) +{ + move(xlines - 2, 0); + clrtoeol(); +} + +#ifdef KEY_RESIZE +/* Clear the old prompt */ +static void clearoldprompt(void) +{ + clearinfoln(); + tolastln(); + addch('\n'); +} +#endif + +/* Messages show up at the bottom */ +static inline void printmsg_nc(const char *msg) +{ + tolastln(); + addstr(msg); + addch('\n'); +} + +static void printmsg(const char *msg) +{ + attron(COLOR_PAIR(cfg.curctx + 1)); + printmsg_nc(msg); + attroff(COLOR_PAIR(cfg.curctx + 1)); +} + +static void printwait(const char *msg, int *presel) +{ + printmsg(msg); + if (presel) { + *presel = MSGWAIT; + if (ndents) + xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1); + } +} + +/* Kill curses and display error before exiting */ +static void printerr(int linenum) +{ + exitcurses(); + perror(xitoa(linenum)); + if (!g_state.picker && selpath) + unlink(selpath); + free(pselbuf); + exit(1); +} + +static inline bool xconfirm(int c) +{ + return (c == 'y' || c == 'Y'); +} + +static int get_input(const char *prompt) +{ + if (prompt) + printmsg(prompt); + cleartimeout(); + + int r = getch(); + +#ifdef KEY_RESIZE + while (r == KEY_RESIZE) { + if (prompt) { + clearoldprompt(); + xlines = LINES; + printmsg(prompt); + } + + r = getch(); + } +#endif + settimeout(); + return r; +} + +static int get_cur_or_sel(void) +{ + if (selbufpos && ndents) { + if (cfg.prefersel) + return 's'; + + int choice = get_input(messages[MSG_CUR_SEL_OPTS]); + + return ((choice == 'c' || choice == 's') ? choice : 0); + } + + if (selbufpos) + return 's'; + + if (ndents) + return 'c'; + + return 0; +} + +static void xdelay(useconds_t delay) +{ + refresh(); + usleep(delay); +} + +static char confirm_force(bool selection) +{ + char str[64]; + + snprintf(str, 64, messages[MSG_FORCE_RM], + (selection ? xitoa(nselected) : "current"), (selection ? "(s)" : "")); + + int r = get_input(str); + + if (r == 27) + return '\0'; /* cancel */ + if (r == 'y' || r == 'Y') + return 'f'; /* forceful */ + return 'i'; /* interactive */ +} + +/* FORK FUNCTIONS */ + +/* No NULL check here as spawn() guards against it */ +static int parseargs(char *line, char **argv) +{ + int count = 0; + + argv[count++] = line; + + while (*line) { // NOLINT + if (ISBLANK(*line)) { + *line++ = '\0'; + + if (!*line) // NOLINT + return count; + + argv[count++] = line; + if (count == EXEC_ARGS_MAX) + return -1; + } + + ++line; + } + + return count; +} + +static pid_t xfork(uchar flag) +{ + int status; + pid_t p = fork(); + struct sigaction dfl_act = {.sa_handler = SIG_DFL}; + + if (p > 0) { + /* the parent ignores the interrupt, quit and hangup signals */ + sigaction(SIGHUP, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsighup); + sigaction(SIGTSTP, &dfl_act, &oldsigtstp); + } else if (p == 0) { + /* We create a grandchild to detach */ + if (flag & F_NOWAIT) { + p = fork(); + + if (p > 0) + _exit(EXIT_SUCCESS); + else if (p == 0) { + sigaction(SIGHUP, &dfl_act, NULL); + sigaction(SIGINT, &dfl_act, NULL); + sigaction(SIGQUIT, &dfl_act, NULL); + sigaction(SIGTSTP, &dfl_act, NULL); + + setsid(); + return p; + } + + perror("fork"); + _exit(EXIT_FAILURE); + } + + /* so they can be used to stop the child */ + sigaction(SIGHUP, &dfl_act, NULL); + sigaction(SIGINT, &dfl_act, NULL); + sigaction(SIGQUIT, &dfl_act, NULL); + sigaction(SIGTSTP, &dfl_act, NULL); + } + + /* This is the parent waiting for the child to create grandchild*/ + if (flag & F_NOWAIT) + waitpid(p, &status, 0); + + if (p == -1) + perror("fork"); + return p; +} + +static int join(pid_t p, uchar flag) +{ + int status = 0xFFFF; + + if (!(flag & F_NOWAIT)) { + /* wait for the child to exit */ + do { + } while (waitpid(p, &status, 0) == -1); + + if (WIFEXITED(status)) { + status = WEXITSTATUS(status); + DPRINTF_D(status); + } + } + + /* restore parent's signal handling */ + sigaction(SIGHUP, &oldsighup, NULL); + sigaction(SIGTSTP, &oldsigtstp, NULL); + + return status; +} + +/* + * Spawns a child process. Behaviour can be controlled using flag. + * Limited to 2 arguments to a program, flag works on bit set. + */ +static int spawn(char *file, char *arg1, char *arg2, uchar flag) +{ + pid_t pid; + int status = 0, retstatus = 0xFFFF; + char *argv[EXEC_ARGS_MAX] = {0}; + char *cmd = NULL; + + if (!file || !*file) + return retstatus; + + /* Swap args if the first arg is NULL and second isn't */ + if (!arg1 && arg2) { + arg1 = arg2; + arg2 = NULL; + } + + if (flag & F_MULTI) { + size_t len = xstrlen(file) + 1; + + cmd = (char *)malloc(len); + if (!cmd) { + DPRINTF_S("malloc()!"); + return retstatus; + } + + xstrsncpy(cmd, file, len); + status = parseargs(cmd, argv); + if (status == -1 || status > (EXEC_ARGS_MAX - 3)) { /* arg1, arg2 and last NULL */ + free(cmd); + DPRINTF_S("NULL or too many args"); + return retstatus; + } + } else + argv[status++] = file; + + argv[status] = arg1; + argv[++status] = arg2; + + if (flag & F_NORMAL) + exitcurses(); + + pid = xfork(flag); + if (pid == 0) { + /* Suppress stdout and stderr */ + if (flag & F_NOTRACE) { + int fd = open("/dev/null", O_WRONLY, 0200); + + dup2(fd, 1); + dup2(fd, 2); + close(fd); + } + + execvp(*argv, argv); + _exit(EXIT_SUCCESS); + } else { + retstatus = join(pid, flag); + + DPRINTF_D(pid); + + if ((flag & F_CONFIRM) || ((flag & F_CHKRTN) && retstatus)) { + printf("%s", messages[MSG_CONTINUE]); +#ifndef NORL + fflush(stdout); +#endif + while (getchar() != '\n'); + } + + if (flag & F_NORMAL) + refresh(); + + free(cmd); + } + + return retstatus; +} + +/* SELECTION HANDLER FUNCTIONS */ + +/* Writes buflen char(s) from buf to a file */ +static void writesel(const char *buf, const size_t buflen) +{ + if (g_state.pickraw || !selpath) + return; + + FILE *fp = fopen(selpath, "w"); + + if (fp) { + if (fwrite(buf, 1, buflen, fp) != buflen) + printwarn(NULL); + fclose(fp); + } else + printwarn(NULL); +} + +static void appendfpath(const char *path, const size_t len) +{ + if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) { + selbuflen += PATH_MAX; + pselbuf = xrealloc(pselbuf, selbuflen); + if (!pselbuf) + errexit(); + } + + selbufpos += xstrsncpy(pselbuf + selbufpos, path, len); +} + +/* Write selected file paths to fd, linefeed separated */ +static size_t seltofile(int fd, uint *pcount) +{ + uint lastpos, count = 0; + char *pbuf = pselbuf; + size_t pos = 0; + ssize_t len, prefixlen = 0, initlen = 0; + + if (pcount) + *pcount = 0; + + if (!selbufpos) + return 0; + + lastpos = selbufpos - 1; + + if (listpath) { + prefixlen = (ssize_t)xstrlen(listroot); + initlen = (ssize_t)xstrlen(listpath); + } + + while (pos <= lastpos) { + DPRINTF_S(pbuf); + len = (ssize_t)xstrlen(pbuf); + + if (!listpath || !is_prefix(pbuf, listpath, initlen)) { + if (write(fd, pbuf, len) != len) + return pos; + } else { + if (write(fd, listroot, prefixlen) != prefixlen) + return pos; + if (write(fd, pbuf + initlen, len - initlen) != (len - initlen)) + return pos; + } + + pos += len; + if (pos <= lastpos) { + if (write(fd, "\n", 1) != 1) + return pos; + pbuf += len + 1; + } + ++pos; + ++count; + } + + if (pcount) + *pcount = count; + + return pos; +} + +/* List selection from selection file (another instance) */ +static bool listselfile(void) +{ + struct stat sb; + + if (stat(selpath, &sb) == -1) + return FALSE; + + /* Nothing selected if file size is 0 */ + if (!sb.st_size) + return FALSE; + + snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < %s", selpath); + spawn(utils[UTIL_SH_EXEC], g_buf, NULL, F_CLI | F_CONFIRM); + + return TRUE; +} + +/* Reset selection indicators */ +static void resetselind(void) +{ + for (int r = 0; r < ndents; ++r) + if (pdents[r].flags & FILE_SELECTED) + pdents[r].flags &= ~FILE_SELECTED; +} + +static void startselection(void) +{ + if (!g_state.selmode) { + g_state.selmode = 1; + nselected = 0; + + if (selbufpos) { + resetselind(); + writesel(NULL, 0); + selbufpos = 0; + } + + lastappendpos = 0; + } +} + +static void updateselbuf(const char *path, char *newpath) +{ + size_t r; + + for (int i = 0; i < ndents; ++i) + if (pdents[i].flags & FILE_SELECTED) { + r = mkpath(path, pdents[i].name, newpath); + appendfpath(newpath, r); + } +} + +/* Finish selection procedure before an operation */ +static void endselection(void) +{ + int fd; + ssize_t count; + char buf[sizeof(patterns[P_REPLACE]) + PATH_MAX + (TMP_LEN_MAX << 1)]; + + if (g_state.selmode) + g_state.selmode = 0; + + if (!listpath || !selbufpos) + return; + + fd = create_tmp_file(); + if (fd == -1) { + DPRINTF_S("couldn't create tmp file"); + return; + } + + seltofile(fd, NULL); + if (close(fd)) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + return; + } + + snprintf(buf, sizeof(buf), patterns[P_REPLACE], listpath, listroot, g_tmpfpath); + spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI); + + fd = open(g_tmpfpath, O_RDONLY); + if (fd == -1) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + if (unlink(g_tmpfpath)) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + } + return; + } + + count = read(fd, pselbuf, selbuflen); + if (count < 0) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + if (close(fd) || unlink(g_tmpfpath)) { + DPRINTF_S(strerror(errno)); + } + return; + } + + if (close(fd) || unlink(g_tmpfpath)) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + return; + } + + selbufpos = count; + pselbuf[--count] = '\0'; + for (--count; count > 0; --count) + if (pselbuf[count] == '\n' && pselbuf[count+1] == '/') + pselbuf[count] = '\0'; + + writesel(pselbuf, selbufpos - 1); +} + +static void clearselection(void) +{ + nselected = 0; + selbufpos = 0; + g_state.selmode = 0; + writesel(NULL, 0); +} + +/* Returns: 1 - success, 0 - none selected, -1 - other failure */ +static int editselection(void) +{ + int ret = -1; + int fd, lines = 0; + ssize_t count; + struct stat sb; + time_t mtime; + + if (!selbufpos) + return listselfile(); + + fd = create_tmp_file(); + if (fd == -1) { + DPRINTF_S("couldn't create tmp file"); + return -1; + } + + seltofile(fd, NULL); + if (close(fd)) { + DPRINTF_S(strerror(errno)); + return -1; + } + + /* Save the last modification time */ + if (stat(g_tmpfpath, &sb)) { + DPRINTF_S(strerror(errno)); + unlink(g_tmpfpath); + return -1; + } + mtime = sb.st_mtime; + + spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, F_CLI); + + fd = open(g_tmpfpath, O_RDONLY); + if (fd == -1) { + DPRINTF_S(strerror(errno)); + unlink(g_tmpfpath); + return -1; + } + + fstat(fd, &sb); + + if (mtime == sb.st_mtime) { + DPRINTF_S("selection is not modified"); + unlink(g_tmpfpath); + return 1; + } + + if (sb.st_size > selbufpos) { + DPRINTF_S("edited buffer larger than previous"); + unlink(g_tmpfpath); + goto emptyedit; + } + + count = read(fd, pselbuf, selbuflen); + if (count < 0) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + if (close(fd) || unlink(g_tmpfpath)) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + } + goto emptyedit; + } + + if (close(fd) || unlink(g_tmpfpath)) { + DPRINTF_S(strerror(errno)); + printwarn(NULL); + goto emptyedit; + } + + if (!count) { + ret = 1; + goto emptyedit; + } + + resetselind(); + selbufpos = count; + /* The last character should be '\n' */ + pselbuf[--count] = '\0'; + for (--count; count > 0; --count) { + /* Replace every '\n' that separates two paths */ + if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') { + ++lines; + pselbuf[count] = '\0'; + } + } + + /* Add a line for the last file */ + ++lines; + + if (lines > nselected) { + DPRINTF_S("files added to selection"); + goto emptyedit; + } + + nselected = lines; + writesel(pselbuf, selbufpos - 1); + + return 1; + +emptyedit: + resetselind(); + clearselection(); + return ret; +} + +static bool selsafe(void) +{ + /* Fail if selection file path not generated */ + if (!selpath) { + printmsg(messages[MSG_SEL_MISSING]); + return FALSE; + } + + /* Fail if selection file path isn't accessible */ + if (access(selpath, R_OK | W_OK) == -1) { + errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL); + return FALSE; + } + + return TRUE; +}