1use 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 }
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 continue;
195 }
196 path = expanded;
197 }
198 }
199
200 if paths.contains(&path) {
201 log_debug!("{func}: duplicate path: {}", path.to_string_lossy());
202 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() }; 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(); 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 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 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 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
356pub 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 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 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 options_set_string!(
519 GLOBAL_S_OPTIONS,
520 c!("default-shell"),
521 false,
522 "{}",
523 getshell().to_string_lossy(),
524 );
525
526 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 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 std::process::exit(client_main(osdep_event_init(), argc, argv, flags, feat))
574 }
575}