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