tmux_rs/
tmux.rs

1// Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
2//
3// Permission to use, copy, modify, and distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
12// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
13// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14use crate::*;
15
16use crate::xmalloc::xstrndup;
17
18unsafe extern "C" {
19    // TODO move/remove
20    fn errx(_: c_int, _: *const u8, ...);
21    fn err(_: c_int, _: *const u8, ...);
22
23    fn tzset();
24}
25
26use crate::compat::{S_ISDIR, fdforkpty::getptmfd, getprogname::getprogname};
27use crate::libc::{
28    CLOCK_MONOTONIC, CLOCK_REALTIME, CODESET, EEXIST, F_GETFL, F_SETFL, LC_CTYPE, LC_TIME,
29    O_NONBLOCK, PATH_MAX, S_IRWXO, S_IRWXU, X_OK, access, clock_gettime, fcntl, getcwd, getenv,
30    getpwuid, getuid, lstat, mkdir, nl_langinfo, printf, realpath, setlocale, stat, strcasecmp,
31    strcasestr, strchr, strcspn, strerror, strncmp, strrchr, strstr, timespec,
32};
33
34use crate::compat::getopt::{OPTARG, OPTIND, getopt};
35
36pub static mut GLOBAL_OPTIONS: *mut options = null_mut();
37
38pub static mut GLOBAL_S_OPTIONS: *mut options = null_mut();
39
40pub static mut GLOBAL_W_OPTIONS: *mut options = null_mut();
41
42pub static mut GLOBAL_ENVIRON: *mut environ = null_mut();
43
44pub static mut START_TIME: timeval = timeval {
45    tv_sec: 0,
46    tv_usec: 0,
47};
48
49pub static mut SOCKET_PATH: *const u8 = null_mut();
50
51pub static mut PTM_FD: c_int = -1;
52
53pub static mut SHELL_COMMAND: *mut u8 = null_mut();
54
55pub fn usage() -> ! {
56    eprintln!(
57        "usage: tmux-rs [-2CDlNuVv] [-c shell-command] [-f file] [-L socket-name]\n               [-S socket-path] [-T features] [command [flags]]\n"
58    );
59    std::process::exit(1)
60}
61
62pub unsafe fn getshell() -> *const u8 {
63    unsafe {
64        let shell = getenv(c!("SHELL"));
65        if checkshell(shell) {
66            return shell;
67        }
68
69        let pw = getpwuid(getuid());
70        if !pw.is_null() && checkshell((*pw).pw_shell.cast()) {
71            return (*pw).pw_shell.cast();
72        }
73
74        _PATH_BSHELL
75    }
76}
77
78pub unsafe fn checkshell(shell: *const u8) -> bool {
79    unsafe {
80        if shell.is_null() || *shell != b'/' {
81            return false;
82        }
83        if areshell(shell) != 0 {
84            return false;
85        }
86        if access(shell.cast(), X_OK) != 0 {
87            return false;
88        }
89    }
90    true
91}
92
93pub unsafe fn areshell(shell: *const u8) -> c_int {
94    unsafe {
95        let ptr = strrchr(shell, b'/' as c_int);
96        let ptr = if !ptr.is_null() {
97            ptr.wrapping_add(1)
98        } else {
99            shell
100        };
101        let mut progname = getprogname();
102        if *progname == b'-' {
103            progname = progname.wrapping_add(1);
104        }
105        if libc::strcmp(ptr, progname) == 0 {
106            1
107        } else {
108            0
109        }
110    }
111}
112
113pub unsafe fn expand_path(path: *const u8, home: *const u8) -> *mut u8 {
114    unsafe {
115        let mut expanded: *mut u8 = null_mut();
116        let mut end: *const u8 = null_mut();
117
118        if strncmp(path, c!("~/"), 2) == 0 {
119            if home.is_null() {
120                return null_mut();
121            }
122            return format_nul!("{}{}", _s(home), _s(path.add(1)));
123        }
124
125        if *path == b'$' {
126            end = strchr(path, b'/' as i32);
127            let name = if end.is_null() {
128                xstrdup(path.add(1)).cast().as_ptr()
129            } else {
130                xstrndup(path.add(1), end.addr() - path.addr() - 1)
131                    .cast()
132                    .as_ptr()
133            };
134            let value = environ_find(GLOBAL_ENVIRON, name);
135            free_(name);
136            if value.is_null() {
137                return null_mut();
138            }
139            if end.is_null() {
140                end = c!("");
141            }
142            return format_nul!("{}{}", _s(transmute_ptr((*value).value)), _s(end));
143        }
144
145        xstrdup(path).cast().as_ptr()
146    }
147}
148
149unsafe fn expand_paths(s: &str, paths: *mut *mut *mut u8, n: *mut u32, ignore_errors: i32) {
150    unsafe {
151        let home = find_home();
152        let mut next: *const u8 = null_mut();
153        let mut resolved: [u8; PATH_MAX as usize] = zeroed(); // TODO use unint version
154        let mut path = null_mut();
155
156        let func = "expand_paths";
157
158        *paths = null_mut();
159        *n = 0;
160
161        let mut tmp: *mut u8 = xstrdup__(s);
162        let copy = tmp;
163        while {
164            next = strsep(&raw mut tmp as _, c!(":").cast());
165            !next.is_null()
166        } {
167            let expanded = expand_path(next, home);
168            if expanded.is_null() {
169                log_debug!("{}: invalid path: {}", func, _s(next));
170                continue;
171            }
172            if realpath(expanded.cast(), resolved.as_mut_ptr()).is_null() {
173                log_debug!(
174                    "{}: realpath(\"{}\") failed: {}",
175                    func,
176                    _s(expanded),
177                    _s(strerror(errno!())),
178                );
179                if ignore_errors != 0 {
180                    free_(expanded);
181                    continue;
182                }
183                path = expanded;
184            } else {
185                path = xstrdup(resolved.as_ptr()).cast().as_ptr();
186                free_(expanded);
187            }
188            let mut i = 0;
189            for j in 0..*n {
190                i = j;
191                if libc::strcmp(path as _, *(*paths).add(i as usize)) == 0 {
192                    break;
193                }
194            }
195            if i != *n {
196                log_debug!("{}: duplicate path: {}", func, _s(path));
197                free_(path);
198                continue;
199            }
200            *paths = xreallocarray_::<*mut u8>(*paths, (*n + 1) as usize).as_ptr();
201            *(*paths).add((*n) as usize) = path;
202            *n += 1;
203        }
204        free_(copy);
205    }
206}
207
208unsafe fn make_label(mut label: *const u8, cause: *mut *mut u8) -> *const u8 {
209    let mut paths: *mut *mut u8 = null_mut();
210    let mut path: *mut u8 = null_mut();
211    let mut base: *mut u8 = null_mut();
212    let mut sb: stat = unsafe { zeroed() }; // TODO use uninit
213    let mut n: u32 = 0;
214
215    unsafe {
216        'fail: {
217            *cause = null_mut();
218            if label.is_null() {
219                label = c!("default");
220            }
221            let uid = getuid();
222
223            expand_paths(TMUX_SOCK, &raw mut paths, &raw mut n, 1);
224            if n == 0 {
225                *cause = format_nul!("no suitable socket path");
226                return null_mut();
227            }
228            path = *paths; /* can only have one socket! */
229            for i in 1..n {
230                free_(*paths.add(i as usize));
231            }
232            free_(paths);
233
234            base = format_nul!("{}/tmux-{}", _s(path), uid);
235            free_(path);
236            if mkdir(base.cast(), S_IRWXU) != 0 && errno!() != EEXIST {
237                *cause = format_nul!(
238                    "couldn't create directory {} ({})",
239                    _s(base),
240                    _s(strerror(errno!()))
241                );
242                break 'fail;
243            }
244            if lstat(base.cast(), &raw mut sb) != 0 {
245                *cause = format_nul!(
246                    "couldn't read directory {} ({})",
247                    _s(base),
248                    _s(strerror(errno!())),
249                );
250                break 'fail;
251            }
252            if !S_ISDIR(sb.st_mode) {
253                *cause = format_nul!("{} is not a directory", _s(base));
254                break 'fail;
255            }
256            if sb.st_uid != uid || (sb.st_mode & S_IRWXO) != 0 {
257                *cause = format_nul!("directory {} has unsafe permissions", _s(base));
258                break 'fail;
259            }
260            path = format_nul!("{}/{}", _s(base), _s(label));
261            free_(base);
262            return path;
263        }
264
265        // fail:
266        free_(base);
267        null_mut()
268    }
269}
270
271pub unsafe fn shell_argv0(shell: *const u8, is_login: c_int) -> *mut u8 {
272    unsafe {
273        let slash = strrchr(shell, b'/' as _);
274        let name = if !slash.is_null() && *slash.add(1) != b'\0' {
275            slash.add(1)
276        } else {
277            shell
278        };
279
280        if is_login != 0 {
281            format_nul!("-{}", _s(name))
282        } else {
283            format_nul!("{}", _s(name))
284        }
285    }
286}
287
288pub unsafe fn setblocking(fd: c_int, state: c_int) {
289    unsafe {
290        let mut mode = fcntl(fd, F_GETFL);
291
292        if mode != -1 {
293            if state == 0 {
294                mode |= O_NONBLOCK;
295            } else {
296                mode &= !O_NONBLOCK;
297            }
298            fcntl(fd, F_SETFL, mode);
299        }
300    }
301}
302
303pub unsafe fn get_timer() -> u64 {
304    unsafe {
305        let mut ts: timespec = zeroed();
306        //We want a timestamp in milliseconds suitable for time measurement,
307        //so prefer the monotonic clock.
308        if clock_gettime(CLOCK_MONOTONIC, &raw mut ts) != 0 {
309            clock_gettime(CLOCK_REALTIME, &raw mut ts);
310        }
311        (ts.tv_sec as u64 * 1000) + (ts.tv_nsec as u64 / 1000000)
312    }
313}
314
315pub unsafe fn find_cwd() -> *mut u8 {
316    static mut CWD: [u8; PATH_MAX as usize] = [0; PATH_MAX as usize];
317    unsafe {
318        let mut resolved1: [u8; PATH_MAX as usize] = [0; PATH_MAX as usize];
319        let mut resolved2: [u8; PATH_MAX as usize] = [0; PATH_MAX as usize];
320
321        if getcwd(&raw mut CWD as _, size_of::<[u8; PATH_MAX as usize]>()).is_null() {
322            return null_mut();
323        }
324        let pwd = getenv(c!("PWD"));
325        if pwd.is_null() || *pwd == b'\0' {
326            return &raw mut CWD as _;
327        }
328
329        //We want to use PWD so that symbolic links are maintained,
330        //but only if it matches the actual working directory.
331
332        if realpath(pwd, &raw mut resolved1 as _).is_null() {
333            return &raw mut CWD as _;
334        }
335        if realpath(&raw mut CWD as _, &raw mut resolved2 as _).is_null() {
336            return &raw mut CWD as _;
337        }
338        if libc::strcmp(&raw mut resolved1 as _, &raw mut resolved2 as _) != 0 {
339            return &raw mut CWD as _;
340        }
341        pwd
342    }
343}
344
345pub unsafe fn find_home() -> *mut u8 {
346    static mut HOME: *mut u8 = null_mut();
347
348    unsafe {
349        if !HOME.is_null() {
350            HOME
351        } else {
352            HOME = getenv(c!("HOME"));
353            if HOME.is_null() || *HOME == b'\0' {
354                let pw = getpwuid(getuid());
355                if !pw.is_null() {
356                    HOME = (*pw).pw_dir.cast();
357                } else {
358                    HOME = null_mut();
359                }
360            }
361
362            HOME
363        }
364    }
365}
366
367pub fn getversion() -> &'static str {
368    crate::TMUX_VERSION
369}
370
371/// entrypoint for tmux binary
372pub unsafe fn tmux_main(mut argc: i32, mut argv: *mut *mut u8, env: *mut *mut u8) {
373    std::panic::set_hook(Box::new(|panic_info| {
374        let backtrace = std::backtrace::Backtrace::capture();
375        let err_str = format!("{backtrace:#?}");
376        std::fs::write("client-panic.txt", err_str).unwrap();
377    }));
378
379    unsafe {
380        // setproctitle_init(argc, argv.cast(), env.cast());
381        let mut cause: *mut u8 = null_mut();
382        let mut path: *const u8 = null_mut();
383        let mut label: *mut u8 = null_mut();
384        let mut feat: i32 = 0;
385        let mut fflag: i32 = 0;
386        let mut flags: client_flag = client_flag::empty();
387
388        if setlocale(LC_CTYPE, c"en_US.UTF-8".as_ptr()).is_null()
389            && setlocale(LC_CTYPE, c"C.UTF-8".as_ptr()).is_null()
390        {
391            if setlocale(LC_CTYPE, c"".as_ptr()).is_null() {
392                errx(1, c!("invalid LC_ALL, LC_CTYPE or LANG"));
393            }
394            let s: *mut u8 = nl_langinfo(CODESET).cast();
395            if strcasecmp(s, c!("UTF-8")) != 0 && strcasecmp(s, c!("UTF8")) != 0 {
396                errx(1, c!("need UTF-8 locale (LC_CTYPE) but have %s"), s);
397            }
398        }
399
400        setlocale(LC_TIME, c"".as_ptr());
401        tzset();
402
403        if **argv == b'-' {
404            flags = client_flag::LOGIN;
405        }
406
407        GLOBAL_ENVIRON = environ_create().as_ptr();
408
409        let mut var = environ;
410        while !(*var).is_null() {
411            environ_put(GLOBAL_ENVIRON, *var, 0);
412            var = var.add(1);
413        }
414
415        let cwd = find_cwd();
416        if !cwd.is_null() {
417            environ_set!(GLOBAL_ENVIRON, c!("PWD"), 0, "{}", _s(cwd));
418        }
419        expand_paths(TMUX_CONF, &raw mut CFG_FILES, &raw mut CFG_NFILES, 1);
420
421        let mut opt;
422        while {
423            opt = getopt(argc, argv.cast(), c"2c:CDdf:lL:NqS:T:uUvV".as_ptr().cast());
424            opt != -1
425        } {
426            match opt as u8 {
427                b'2' => tty_add_features(&raw mut feat, c!("256"), c!(":,")),
428                b'c' => SHELL_COMMAND = OPTARG.cast(),
429                b'D' => flags |= client_flag::NOFORK,
430                b'C' => {
431                    if flags.intersects(client_flag::CONTROL) {
432                        flags |= client_flag::CONTROLCONTROL;
433                    } else {
434                        flags |= client_flag::CONTROL;
435                    }
436                }
437                b'f' => {
438                    if fflag == 0 {
439                        fflag = 1;
440                        for i in 0..CFG_NFILES {
441                            free((*CFG_FILES.add(i as usize)) as _);
442                        }
443                        CFG_NFILES = 0;
444                    }
445                    CFG_FILES =
446                        xreallocarray_::<*mut u8>(CFG_FILES, CFG_NFILES as usize + 1).as_ptr();
447                    *CFG_FILES.add(CFG_NFILES as usize) = xstrdup(OPTARG.cast()).cast().as_ptr();
448                    CFG_NFILES += 1;
449                    CFG_QUIET.store(false, atomic::Ordering::Relaxed);
450                }
451                b'V' => {
452                    println!("tmux {}", getversion());
453                    std::process::exit(0);
454                }
455                b'l' => flags |= client_flag::LOGIN,
456                b'L' => {
457                    free(label as _);
458                    label = xstrdup(OPTARG.cast()).cast().as_ptr();
459                }
460                b'N' => flags |= client_flag::NOSTARTSERVER,
461                b'q' => (),
462                b'S' => {
463                    free(path as _);
464                    path = xstrdup(OPTARG.cast()).cast().as_ptr();
465                }
466                b'T' => tty_add_features(&raw mut feat, OPTARG.cast(), c!(":,")),
467                b'u' => flags |= client_flag::UTF8,
468                b'v' => log_add_level(),
469                _ => usage(),
470            }
471        }
472        argc -= OPTIND;
473        argv = argv.add(OPTIND as usize);
474
475        if !SHELL_COMMAND.is_null() && argc != 0 {
476            usage();
477        }
478        if flags.intersects(client_flag::NOFORK) && argc != 0 {
479            usage();
480        }
481
482        PTM_FD = getptmfd();
483        if PTM_FD == -1 {
484            err(1, c!("getptmfd"));
485        }
486
487        /*
488        // TODO no pledge on linux
489            if pledge("stdio rpath wpath cpath flock fattr unix getpw sendfd recvfd proc exec tty ps", null_mut()) != 0 {
490                err(1, "pledge");
491        }
492        */
493
494        // tmux is a UTF-8 terminal, so if TMUX is set, assume UTF-8.
495        // Otherwise, if the user has set LC_ALL, LC_CTYPE or LANG to contain
496        // UTF-8, it is a safe assumption that either they are using a UTF-8
497        // terminal, or if not they know that output from UTF-8-capable
498        // programs may be wrong.
499        if !getenv(c!("TMUX")).is_null() {
500            flags |= client_flag::UTF8;
501        } else {
502            let mut s = getenv(c!("LC_ALL")) as *const u8;
503            if s.is_null() || *s == b'\0' {
504                s = getenv(c!("LC_CTYPE")) as *const u8;
505            }
506            if s.is_null() || *s == b'\0' {
507                s = getenv(c!("LANG")) as *const u8;
508            }
509            if s.is_null() || *s == b'\0' {
510                s = c!("");
511            }
512            if !strcasestr(s, c!("UTF-8")).is_null() || !strcasestr(s, c!("UTF8")).is_null() {
513                flags |= client_flag::UTF8;
514            }
515        }
516
517        GLOBAL_OPTIONS = options_create(null_mut());
518        GLOBAL_S_OPTIONS = options_create(null_mut());
519        GLOBAL_W_OPTIONS = options_create(null_mut());
520
521        let mut oe: *const options_table_entry = &raw const OPTIONS_TABLE as _;
522        while !(*oe).name.is_null() {
523            if (*oe).scope & OPTIONS_TABLE_SERVER != 0 {
524                options_default(GLOBAL_OPTIONS, oe);
525            }
526            if (*oe).scope & OPTIONS_TABLE_SESSION != 0 {
527                options_default(GLOBAL_S_OPTIONS, oe);
528            }
529            if (*oe).scope & OPTIONS_TABLE_WINDOW != 0 {
530                options_default(GLOBAL_W_OPTIONS, oe);
531            }
532            oe = oe.add(1);
533        }
534
535        // The default shell comes from SHELL or from the user's passwd entry if available.
536        options_set_string!(
537            GLOBAL_S_OPTIONS,
538            c!("default-shell"),
539            0,
540            "{}",
541            _s(getshell()),
542        );
543
544        // Override keys to vi if VISUAL or EDITOR are set.
545        let mut s = getenv(c!("VISUAL"));
546        if !s.is_null()
547            || ({
548                s = getenv(c!("EDITOR"));
549                !s.is_null()
550            })
551        {
552            options_set_string!(GLOBAL_OPTIONS, c!("editor"), 0, "{}", _s(s));
553            if !strrchr(s, b'/' as _).is_null() {
554                s = strrchr(s, b'/' as _).add(1);
555            }
556            let keys = if !strstr(s, c!("vi")).is_null() {
557                modekey::MODEKEY_VI
558            } else {
559                modekey::MODEKEY_EMACS
560            };
561            options_set_number(GLOBAL_S_OPTIONS, c!("status-keys"), keys as _);
562            options_set_number(GLOBAL_W_OPTIONS, c!("mode-keys"), keys as _);
563        }
564
565        // If socket is specified on the command-line with -S or -L, it is
566        // used. Otherwise, $TMUX is checked and if that fails "default" is
567        // used.
568        if path.is_null() && label.is_null() {
569            s = getenv(c!("TMUX"));
570            if !s.is_null() && *s != b'\0' && *s != b',' {
571                let tmp: *mut u8 = xstrdup(s).cast().as_ptr();
572                *tmp.add(strcspn(tmp, c!(","))) = b'\0';
573                path = tmp;
574            }
575        }
576        if path.is_null() {
577            path = make_label(label.cast(), &raw mut cause);
578            if path.is_null() {
579                if !cause.is_null() {
580                    libc::fprintf(stderr, c"%s\n".as_ptr(), cause);
581                    free(cause as _);
582                }
583                std::process::exit(1);
584            }
585            flags |= client_flag::DEFAULTSOCKET;
586        }
587        SOCKET_PATH = path;
588        free_(label);
589
590        // Pass control to the client.
591        std::process::exit(client_main(osdep_event_init(), argc, argv, flags, feat))
592    }
593}