1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3 clippy::let_underscore_untyped,
4 reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7 clippy::question_mark,
8 reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13 clippy::unnecessary_lazy_evaluations,
14 reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
15)]
16#![cfg_attr(
17 test,
18 allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
19)]
20#![allow(unknown_lints)]
21#![warn(
22 missing_copy_implementations,
23 missing_debug_implementations,
24 missing_docs,
25 rust_2024_compatibility,
26 trivial_casts,
27 trivial_numeric_casts,
28 unused_qualifications,
29 variant_size_differences
30)]
31#![forbid(unsafe_code)]
32#![cfg_attr(docsrs, feature(doc_cfg))]
37#![cfg_attr(docsrs, feature(doc_alias))]
38
39#[cfg(doctest)]
64#[doc = include_str!("../README.md")]
65mod readme {}
66
67use std::env;
68use std::fs;
69use std::path::PathBuf;
70
71use bstr::ByteSlice;
72
73#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)]
75pub enum EditMode {
76 #[default]
80 Emacs,
81 Vi,
83}
84
85#[cfg(feature = "rustyline")]
86#[cfg_attr(docsrs, doc(cfg(feature = "rustyline")))]
87impl From<EditMode> for rustyline::config::EditMode {
88 fn from(mode: EditMode) -> Self {
89 match mode {
90 EditMode::Emacs => Self::Emacs,
91 EditMode::Vi => Self::Vi,
92 }
93 }
94}
95
96#[must_use]
126pub fn rl_read_init_file() -> Option<Vec<u8>> {
127 if let Some(inputrc) = env::var_os("INPUTRC") {
128 return fs::read(inputrc).ok();
129 }
130
131 let home_dir = home_dir();
132 if let Some(ref home_dir) = home_dir {
133 let inputrc = home_dir.join(".inputrc");
134 if let Ok(content) = fs::read(inputrc) {
135 return Some(content);
136 }
137 }
138
139 if let Ok(content) = fs::read("/etc/inputrc") {
140 return Some(content);
141 }
142
143 if cfg!(windows) {
144 if let Some(home_dir) = home_dir {
145 let inputrc = home_dir.join("_inputrc");
146 if let Ok(content) = fs::read(inputrc) {
147 return Some(content);
148 }
149 }
150 }
151
152 None
153}
154
155#[cfg(not(any(unix, windows)))]
156fn home_dir() -> Option<PathBuf> {
157 None
158}
159
160#[cfg(unix)]
161fn home_dir() -> Option<PathBuf> {
162 #[allow(deprecated)]
176 env::home_dir()
177}
178
179#[cfg(windows)]
180fn home_dir() -> Option<PathBuf> {
181 use known_folders::{KnownFolder, get_known_folder_path};
182
183 get_known_folder_path(KnownFolder::Profile)
184}
185
186#[must_use]
223pub fn get_readline_edit_mode(contents: impl AsRef<[u8]>) -> Option<EditMode> {
224 fn inner(contents: &[u8]) -> Option<EditMode> {
225 let mut edit_mode = None; for line in contents.lines() {
228 let line = trim_whitespace_front(line);
230
231 if matches!(line.first(), Some(b'#') | None) {
233 continue;
234 }
235
236 let line = match line.strip_prefix(b"set") {
238 Some(rest) => rest,
239 None => continue,
240 };
241 let line = trim_whitespace_front(line);
243
244 let line = match line {
252 [
253 b'e' | b'E',
254 b'd' | b'D',
255 b'i' | b'I',
256 b't' | b'T',
257 b'i' | b'I',
258 b'n' | b'N',
259 b'g' | b'G',
260 b'-',
261 b'm' | b'M',
262 b'o' | b'O',
263 b'd' | b'D',
264 b'e' | b'E',
265 rest @ ..,
266 ] => rest,
267 _ => continue,
268 };
269 let line = trim_whitespace_front(line);
271
272 match line {
281 [b'v' | b'V', b'i' | b'I'] => {
282 edit_mode = Some(EditMode::Vi);
284 }
285 [b'e' | b'E', b'm' | b'M', b'a' | b'A', b'c' | b'C', b's' | b'S'] => {
286 edit_mode = Some(EditMode::Emacs);
288 }
289 [b'v' | b'V', b'i' | b'I', next, ..] if posix_space::is_space(*next) => {
290 edit_mode = Some(EditMode::Vi);
292 }
293 [
294 b'e' | b'E',
295 b'm' | b'M',
296 b'a' | b'A',
297 b'c' | b'C',
298 b's' | b'S',
299 next,
300 ..,
301 ] if posix_space::is_space(*next) => {
302 edit_mode = Some(EditMode::Emacs);
304 }
305 _ => {}
308 }
309 }
310
311 edit_mode
312 }
313
314 inner(contents.as_ref())
315}
316
317fn trim_whitespace_front(mut s: &[u8]) -> &[u8] {
319 loop {
320 if let Some((&head, tail)) = s.split_first() {
321 if posix_space::is_space(head) {
322 s = tail;
323 continue;
324 }
325 }
326 break s;
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::{EditMode, get_readline_edit_mode};
333
334 #[test]
335 fn test_default_edit_mode_is_emacs() {
336 assert_eq!(EditMode::default(), EditMode::Emacs);
337 }
338
339 #[test]
340 #[cfg(feature = "rustyline")]
341 fn test_edit_mode_rustyline_into() {
342 assert_eq!(rustyline::config::EditMode::Emacs, EditMode::Emacs.into());
343 assert_eq!(rustyline::config::EditMode::Vi, EditMode::Vi.into());
344 }
345
346 #[test]
347 fn test_get_readline_edit_mode_vi_mode() {
348 let config = "set editing-mode vi";
349 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
350 }
351
352 #[test]
353 fn test_get_readline_edit_mode_emacs_mode() {
354 let config = "set editing-mode emacs";
355 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
356 }
357
358 #[test]
359 fn test_get_readline_edit_mode_parse_empty_and_blank_lines() {
360 let test_cases = [
361 "",
362 " ",
363 "\t\t\t",
364 " \n ",
365 "\n",
366 "\r\n",
367 " \r\n ",
368 ];
369 for contents in test_cases {
370 assert_eq!(get_readline_edit_mode(contents), None);
371 }
372 }
373
374 #[test]
375 fn test_get_readline_edit_mode_whitespace_only_lines() {
376 let contents = "
377 \t
378 \n
379 \r
380 ";
381 assert_eq!(get_readline_edit_mode(contents), None);
382 }
383
384 #[test]
385 fn test_get_readline_edit_mode_empty_contents() {
386 let contents = "";
387 assert_eq!(get_readline_edit_mode(contents), None);
388 }
389
390 #[test]
391 fn test_get_readline_edit_mode_no_set_directive() {
392 let contents = "editing-mode vi";
393 assert_eq!(get_readline_edit_mode(contents), None);
394 }
395
396 #[test]
397 fn test_get_readline_edit_mode_comment_lines() {
398 let contents = "
399 # This is a comment
400 # set editing-mode vi
401 # set editing-mode emacs
402 ";
403 assert_eq!(get_readline_edit_mode(contents), None);
404 }
405
406 #[test]
407 fn test_get_readline_edit_mode_set_editing_mode_with_space_before_variable_name() {
408 let test_cases = [
409 ("set editing-mode vi", EditMode::Vi),
410 ("set editing-mode emacs", EditMode::Emacs),
411 ];
412
413 for (config, expected_mode) in test_cases {
414 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
415 }
416 }
417
418 #[test]
419 fn test_get_readline_edit_mode_set_editing_mode_with_space_after_variable_name() {
420 let test_cases = [
421 ("set editing-mode vi", EditMode::Vi),
422 ("set editing-mode emacs", EditMode::Emacs),
423 ];
424
425 for (config, expected_mode) in test_cases {
426 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
427 }
428 }
429
430 #[test]
431 fn test_get_readline_edit_mode_excess_whitespace() {
432 let test_cases = [
433 ("set editing-mode \t vi \t \r\n", EditMode::Vi),
434 ("set editing-mode \t emacs \t \r\n", EditMode::Emacs),
435 ("set editing-mode vi \t \n", EditMode::Vi),
436 ("set editing-mode emacs \t \n", EditMode::Emacs),
437 ];
438
439 for (config, expected_mode) in test_cases {
440 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
441 }
442 }
443
444 #[test]
445 fn test_get_readline_edit_mode_ignore_trailing_garbage() {
446 let test_cases = [
447 ("set editing-mode vi 1234", EditMode::Vi),
448 ("set editing-mode emacs 1234", EditMode::Emacs),
449 ("set editing-mode vi this-is-extra-content", EditMode::Vi),
450 ("set editing-mode emacs this-is-extra-content", EditMode::Emacs),
451 ];
452
453 for (config, expected_mode) in test_cases {
454 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
455 }
456 }
457
458 #[test]
459 fn test_get_readline_edit_mode_requires_lowercase_set() {
460 let test_cases = [
461 "SET editing-mode vi",
462 "SET editing-mode emacs",
463 "Set editing-mode vi",
464 "Set editing-mode emacs",
465 "sET editing-mode vi",
466 "sET editing-mode emacs",
467 ];
468
469 for config in test_cases {
470 assert_eq!(get_readline_edit_mode(config), None);
471 }
472 }
473
474 #[test]
475 fn test_get_readline_editing_mode_variable_name_case_insensitive() {
476 let test_cases = [
477 ("set editing-mode vi", EditMode::Vi),
479 ("set editing-mode emacs", EditMode::Emacs),
480 ("set EDITING-MODE emacs", EditMode::Emacs),
482 ("set EDITING-MODE vi", EditMode::Vi),
483 ("set Editing-Mode vi", EditMode::Vi),
485 ("set Editing-Mode emacs", EditMode::Emacs),
486 ("set EdItInG-MoDe vi", EditMode::Vi),
487 ("set EdItInG-MoDe emacs", EditMode::Emacs),
488 ("set eDiTiNg-mOdE vi", EditMode::Vi),
489 ("set eDiTiNg-mOdE emacs", EditMode::Emacs),
490 ];
491
492 for (config, expected_mode) in test_cases {
493 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
494 }
495 }
496
497 #[test]
498 fn test_get_readline_editing_mode_variable_value_case_insensitive() {
499 let test_cases = [
500 ("set editing-mode vi", EditMode::Vi),
502 ("set editing-mode emacs", EditMode::Emacs),
503 ("set editing-mode VI", EditMode::Vi),
505 ("set editing-mode EMACS", EditMode::Emacs),
506 ("set editing-mode Vi", EditMode::Vi),
508 ("set editing-mode vI", EditMode::Vi),
509 ("set editing-mode eMaCs", EditMode::Emacs),
510 ("set editing-mode EmAcS", EditMode::Emacs),
511 ("set editing-mode emACS", EditMode::Emacs),
512 ("set editing-mode EMacs", EditMode::Emacs),
513 ];
514
515 for (config, expected_mode) in test_cases {
516 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
517 }
518 }
519
520 #[test]
521 fn test_get_readline_editing_mode_ignores_unrecognized_variable_names() {
522 let input = "set unknown-variable foo";
524 assert_eq!(get_readline_edit_mode(input), None);
525 }
526
527 #[test]
528 fn test_get_readline_edit_mode_multiple_lines_with_comments() {
529 let contents = "
530 # This is a comment
531 set some-other-setting 123
532
533 # Another comment
534 set editing-mode vi
535
536 # One more comment
537 set another-setting true
538 ";
539 assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
540 }
541
542 #[test]
543 fn test_get_readline_edit_mode_no_mode_directive() {
544 let config = "set blink-matching-paren on\n";
545 assert_eq!(get_readline_edit_mode(config), None);
546 }
547
548 #[test]
549 fn test_get_readline_edit_mode_multiple_lines() {
550 let config = "set editing-mode vi\nset blink-matching-paren on\n";
551 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
552 }
553
554 #[test]
555 fn test_get_readline_edit_mode_invalid_variable_value() {
556 let config = "set editing-mode vim\n";
557 assert_eq!(get_readline_edit_mode(config), None);
558 }
559
560 #[test]
561 fn test_get_readline_edit_mode_invalid_characters_variable_value() {
562 let config = "set editing-mode vī\n";
563 assert_eq!(get_readline_edit_mode(config), None);
564 }
565
566 #[test]
567 fn test_get_readline_edit_mode_with_posix_spaces() {
568 let test_cases = [
569 ("set editing-mode vi", EditMode::Vi),
570 ("set editing-mode emacs", EditMode::Emacs),
571 ("set editing-mode\tvi", EditMode::Vi),
572 ("set editing-mode\temacs", EditMode::Emacs),
573 ("set editing-mode \t \tvi", EditMode::Vi),
574 ("set editing-mode \t \temacs", EditMode::Emacs),
575 ("set editing-mode\t\t\t\t\tvi", EditMode::Vi),
576 ("set editing-mode\t\t\t\t\temacs", EditMode::Emacs),
577 ];
578
579 for (config, expected_mode) in test_cases {
580 assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
581 }
582 }
583
584 #[test]
585 fn test_get_readline_edit_mode_vi_mode_with_multibyte_utf8() {
586 let config = "set editing-mode vī\n";
587 assert_eq!(get_readline_edit_mode(config), None);
588 }
589
590 #[test]
591 fn test_get_readline_edit_mode_emacs_mode_with_multibyte_utf8() {
592 let config = "set editing-mode eĦmacs\n";
593 assert_eq!(get_readline_edit_mode(config), None);
594 }
595
596 #[test]
597 fn test_get_readline_edit_mode_vi_mode_with_trailing_invalid_utf8() {
598 let config = b"set editing-mode vi \x80\n";
599 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
600 }
601
602 #[test]
603 fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode() {
604 let config = b"set editing-mode v\xFFi\n";
605 assert_eq!(get_readline_edit_mode(config), None);
606 }
607
608 #[test]
609 fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode() {
610 let config = b"set editing-mode e\xEFmacs\n";
611 assert_eq!(get_readline_edit_mode(config), None);
612 }
613
614 #[test]
615 fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_with_trailing_content() {
616 let config = b"set editing-mode vi \xFF\xFF\xFF\n";
617 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
618 }
619
620 #[test]
621 fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_with_trailing_content() {
622 let config = b"set editing-mode emacs this-\x80is-extra-content\n";
623 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
624 }
625
626 #[test]
627 fn test_get_readline_edit_mode_invalid_utf8_bytes_multiple_lines() {
628 let config = b"set editing-mode vi\nset blink-matching-paren \xC0\x80on\n";
629 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
630 }
631
632 #[test]
633 fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_excess_whitespace() {
634 let config = b"set editing-mode \x80emacs \t \n";
635 assert_eq!(get_readline_edit_mode(config), None);
636 }
637
638 #[test]
639 fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_excess_whitespace() {
640 let config = b"set editing-mode \x80vi \t \r\n";
641 assert_eq!(get_readline_edit_mode(config), None);
642 }
643
644 #[test]
645 fn test_get_readline_edit_mode_invalid_utf8_bytes_no_mode_directive() {
646 let config = b"set blink-matching-\x80paren on\n";
647 assert_eq!(get_readline_edit_mode(config), None);
648 }
649
650 #[test]
651 fn test_get_readline_edit_mode_invalid_utf8_bytes_invalid_directive() {
652 let config = b"set editing-\x80mode vim\n";
653 assert_eq!(get_readline_edit_mode(config), None);
654 }
655
656 #[test]
657 fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_with_posix_spaces() {
658 let config = b"set editing-mode e\xEFmacs\n";
659 assert_eq!(get_readline_edit_mode(config), None);
660 }
661
662 #[test]
663 fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_with_posix_spaces() {
664 let config = b"set editing-\x80mode\tvi\n";
665 assert_eq!(get_readline_edit_mode(config), None);
666 }
667
668 #[test]
669 fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_with_multiple_posix_spaces() {
670 let config = b"set editing-mode \t \nem\xF4cs\n";
671 assert_eq!(get_readline_edit_mode(config), None);
672 }
673
674 #[test]
675 fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_with_multiple_posix_spaces() {
676 let config = b"set editing-\xF4mode \t \nvi\n";
677 assert_eq!(get_readline_edit_mode(config), None);
678 }
679
680 #[test]
681 fn test_get_readline_edit_mode_quotes() {
682 let test_cases = [
683 "set editing-mode 'emacs'",
685 "set editing-mode 'vi'",
686 "set editing-mode \"emacs\"",
688 "set editing-mode \"vi\"",
689 "set editing-mode 'emacs\"",
691 "set editing-mode 'vi\"",
692 ];
693
694 for config in test_cases {
695 assert_eq!(get_readline_edit_mode(config), None);
696 }
697 }
698
699 #[test]
700 fn test_get_readline_edit_mode_last_set_directive_vi() {
701 let contents = "
702 set editing-mode emacs
703 set editing-mode vi
704 ";
705 assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
706 }
707
708 #[test]
709 fn test_get_readline_edit_mode_last_set_directive_emacs() {
710 let contents = "
711 set editing-mode vi
712 set editing-mode emacs
713 ";
714 assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Emacs));
715 }
716
717 #[test]
718 fn test_get_readline_edit_mode_last_set_directive_vi_with_whitespace() {
719 let contents = "
720 set editing-mode emacs
721 set editing-mode vi
722 ";
723 assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
724 }
725
726 #[test]
727 fn test_get_readline_edit_mode_last_set_directive_emacs_with_whitespace() {
728 let contents = "
729 set editing-mode vi
730 set editing-mode emacs
731 ";
732 assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Emacs));
733 }
734
735 #[test]
736 fn test_get_readline_edit_mode_multiple_set_directives_mixed() {
737 let contents = "
738 set some-other-setting 123
739
740 set editing-mode vi
741
742 set another-setting true
743
744 set editing-mode emacs
745
746 set extra-setting abc
747 set extra-setting xyz
748
749 set editing-mode vi
750 ";
751 assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
752 }
753
754 #[test]
755 fn test_get_readline_edit_mode_integration_1() {
756 let config = "
757 set blink-matching-paren on
758 set keymap vi-command
759 set editing-mode emacs
760 set completion-ignore-case on
761 ";
762 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
763 }
764
765 #[test]
766 fn test_get_readline_edit_mode_integration_2() {
767 let config = "
768 set blink-matching-paren on
769 set editing-mode vi
770 set completion-ignore-case on
771 set keymap vi-command
772 ";
773 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
774 }
775
776 #[test]
777 fn test_get_readline_edit_mode_integration_3() {
778 let config = "
779 set blink-matching-paren on
780 set completion-ignore-case on
781 set editing-mode emacs
782 set keymap vi-command
783 ";
784 assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
785 }
786
787 #[test]
788 fn test_get_readline_edit_mode_integration_4() {
789 let config = "
790 set blink-matching-paren on
791 set keymap vi-command
792 set completion-ignore-case on
793 ";
794 assert_eq!(get_readline_edit_mode(config), None);
795 }
796
797 #[test]
798 fn test_get_readline_edit_mode_integration_5() {
799 let config = "
800 set blink-matching-paren on
801 set completion-ignore-case on
802 set keymap vi-command
803 ";
804 assert_eq!(get_readline_edit_mode(config), None);
805 }
806
807 #[test]
808 fn test_get_readline_edit_mode_integration_6() {
809 let config = "
810 set blink-matching-paren on
811 set keymap vi-command
812 ";
813 assert_eq!(get_readline_edit_mode(config), None);
814 }
815}