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