Skip to main content

rustyline/
history.rs

1//! History API
2
3#[cfg(feature = "with-file-history")]
4use log::{debug, warn};
5use std::borrow::Cow;
6use std::collections::vec_deque;
7use std::collections::VecDeque;
8#[cfg(feature = "with-file-history")]
9use std::fs::{File, OpenOptions};
10#[cfg(feature = "with-file-history")]
11use std::io::SeekFrom;
12use std::ops::Index;
13use std::path::Path;
14#[cfg(feature = "with-file-history")]
15use std::time::SystemTime;
16
17use super::Result;
18use crate::config::{Config, HistoryDuplicates};
19
20/// Search direction
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum SearchDirection {
23    /// Search history forward
24    Forward,
25    /// Search history backward
26    Reverse,
27}
28
29/// History search result
30#[derive(Debug, Clone, Eq, PartialEq)]
31pub struct SearchResult<'a> {
32    /// history entry
33    pub entry: Cow<'a, str>,
34    /// history index
35    pub idx: usize,
36    /// match position in `entry`
37    pub pos: usize,
38}
39
40/// Interface for navigating/loading/storing history
41// TODO Split navigation part from backend part
42pub trait History {
43    // TODO jline3: interface Entry {
44    //         int index();
45    //         Instant time();
46    //         String line();
47    //     }
48    // replxx: HistoryEntry {
49    // 		std::string _timestamp;
50    // 		std::string _text;
51
52    // termwiz: fn get(&self, idx: HistoryIndex) -> Option<Cow<str>>;
53
54    /// Return the history entry at position `index`, starting from 0.
55    ///
56    /// `SearchDirection` is useful only for implementations without direct
57    /// indexing.
58    fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult<'_>>>;
59
60    // termwiz: fn last(&self) -> Option<HistoryIndex>;
61
62    // jline3: default void add(String line) {
63    //         add(Instant.now(), line);
64    //     }
65    // jline3: void add(Instant time, String line);
66    // termwiz: fn add(&mut self, line: &str);
67    // reedline: fn append(&mut self, entry: &str);
68
69    /// Add a new entry in the history.
70    ///
71    /// Return false if the `line` has been ignored (blank line / duplicate /
72    /// ...).
73    fn add(&mut self, line: &str) -> Result<bool>;
74    /// Add a new entry in the history.
75    ///
76    /// Return false if the `line` has been ignored (blank line / duplicate /
77    /// ...).
78    fn add_owned(&mut self, line: String) -> Result<bool>; // TODO check AsRef<str> + Into<String> vs object safe
79
80    /// Return the number of entries in the history.
81    #[must_use]
82    fn len(&self) -> usize;
83
84    /// Return true if the history has no entry.
85    #[must_use]
86    fn is_empty(&self) -> bool;
87
88    // TODO jline3: int index();
89    // TODO jline3: String current();
90    // reedline: fn string_at_cursor(&self) -> Option<String>;
91    // TODO jline3: boolean previous();
92    // reedline: fn back(&mut self);
93    // TODO jline3: boolean next();
94    // reedline: fn forward(&mut self);
95    // TODO jline3: boolean moveToFirst();
96    // TODO jline3: boolean moveToFirst();
97    // TODO jline3: boolean moveToLast();
98    // TODO jline3: boolean moveTo(int index);
99    // TODO jline3: void moveToEnd();
100    // TODO jline3: void resetIndex();
101
102    // TODO jline3: int first();
103    // TODO jline3: default boolean isPersistable(Entry entry) {
104    //         return true;
105    //     }
106
107    /// Set the maximum length for the history. This function can be called even
108    /// if there is already some history, the function will make sure to retain
109    /// just the latest `len` elements if the new history length value is
110    /// smaller than the amount of items already inside the history.
111    ///
112    /// Like [stifle_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX11).
113    fn set_max_len(&mut self, len: usize) -> Result<()>;
114
115    /// Ignore consecutive duplicates
116    fn ignore_dups(&mut self, yes: bool) -> Result<()>;
117
118    /// Ignore lines which begin with a space or not
119    fn ignore_space(&mut self, yes: bool);
120
121    /// Save the history in the specified file.
122    // TODO history_truncate_file
123    // https://tiswww.case.edu/php/chet/readline/history.html#IDX31
124    fn save(&mut self, path: &Path) -> Result<()>; // FIXME Path vs AsRef<Path>
125
126    /// Append new entries in the specified file.
127    // Like [append_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX30).
128    fn append(&mut self, path: &Path) -> Result<()>; // FIXME Path vs AsRef<Path>
129
130    /// Load the history from the specified file.
131    ///
132    /// # Errors
133    /// Will return `Err` if path does not already exist or could not be read.
134    fn load(&mut self, path: &Path) -> Result<()>; // FIXME Path vs AsRef<Path>
135
136    /// Clear in-memory history
137    fn clear(&mut self) -> Result<()>;
138
139    // termwiz: fn search(
140    //         &self,
141    //         idx: HistoryIndex,
142    //         style: SearchStyle,
143    //         direction: SearchDirection,
144    //         pattern: &str,
145    //     ) -> Option<SearchResult>;
146    // reedline: fn set_navigation(&mut self, navigation: HistoryNavigationQuery);
147    // reedline: fn get_navigation(&self) -> HistoryNavigationQuery;
148
149    /// Search history (start position inclusive [0, len-1]).
150    ///
151    /// Return the absolute index of the nearest history entry that matches
152    /// `term`.
153    ///
154    /// Return None if no entry contains `term` between [start, len -1] for
155    /// forward search
156    /// or between [0, start] for reverse search.
157    fn search(
158        &self,
159        term: &str,
160        start: usize,
161        dir: SearchDirection,
162    ) -> Result<Option<SearchResult<'_>>>;
163
164    /// Anchored search
165    fn starts_with(
166        &self,
167        term: &str,
168        start: usize,
169        dir: SearchDirection,
170    ) -> Result<Option<SearchResult<'_>>>;
171
172    /* TODO How ? DoubleEndedIterator may be difficult to implement (for an SQLite backend)
173    /// Return a iterator.
174    #[must_use]
175    fn iter(&self) -> impl DoubleEndedIterator<Item = &String> + '_;
176     */
177}
178
179/// Transient in-memory history implementation.
180pub struct MemHistory {
181    entries: VecDeque<String>,
182    max_len: usize,
183    ignore_space: bool,
184    ignore_dups: bool,
185}
186
187impl MemHistory {
188    /// Default constructor
189    #[must_use]
190    pub fn new() -> Self {
191        Self::with_config(&Config::default())
192    }
193
194    /// Customized constructor with:
195    /// - [`Config::max_history_size()`],
196    /// - [`Config::history_ignore_space()`],
197    /// - [`Config::history_duplicates()`].
198    #[must_use]
199    pub fn with_config(config: &Config) -> Self {
200        Self {
201            entries: VecDeque::new(),
202            max_len: config.max_history_size(),
203            ignore_space: config.history_ignore_space(),
204            ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
205        }
206    }
207
208    fn search_match<F>(
209        &self,
210        term: &str,
211        start: usize,
212        dir: SearchDirection,
213        test: F,
214    ) -> Option<SearchResult<'_>>
215    where
216        F: Fn(&str) -> Option<usize>,
217    {
218        if term.is_empty() || start >= self.len() {
219            return None;
220        }
221        match dir {
222            SearchDirection::Reverse => {
223                for (idx, entry) in self
224                    .entries
225                    .iter()
226                    .rev()
227                    .skip(self.len() - 1 - start)
228                    .enumerate()
229                {
230                    if let Some(cursor) = test(entry) {
231                        return Some(SearchResult {
232                            idx: start - idx,
233                            entry: Cow::Borrowed(entry),
234                            pos: cursor,
235                        });
236                    }
237                }
238                None
239            }
240            SearchDirection::Forward => {
241                for (idx, entry) in self.entries.iter().skip(start).enumerate() {
242                    if let Some(cursor) = test(entry) {
243                        return Some(SearchResult {
244                            idx: idx + start,
245                            entry: Cow::Borrowed(entry),
246                            pos: cursor,
247                        });
248                    }
249                }
250                None
251            }
252        }
253    }
254
255    fn ignore(&self, line: &str) -> bool {
256        if self.max_len == 0 {
257            return true;
258        }
259        if line.is_empty()
260            || (self.ignore_space && line.chars().next().is_none_or(char::is_whitespace))
261        {
262            return true;
263        }
264        if self.ignore_dups {
265            if let Some(s) = self.entries.back() {
266                if s == line {
267                    return true;
268                }
269            }
270        }
271        false
272    }
273
274    fn insert(&mut self, line: String) {
275        if self.entries.len() == self.max_len {
276            self.entries.pop_front();
277        }
278        self.entries.push_back(line);
279    }
280}
281
282impl Default for MemHistory {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288impl History for MemHistory {
289    fn get(&self, index: usize, _: SearchDirection) -> Result<Option<SearchResult<'_>>> {
290        Ok(self
291            .entries
292            .get(index)
293            .map(String::as_ref)
294            .map(Cow::Borrowed)
295            .map(|entry| SearchResult {
296                entry,
297                idx: index,
298                pos: 0,
299            }))
300    }
301
302    fn add(&mut self, line: &str) -> Result<bool> {
303        if self.ignore(line) {
304            return Ok(false);
305        }
306        self.insert(line.to_owned());
307        Ok(true)
308    }
309
310    fn add_owned(&mut self, line: String) -> Result<bool> {
311        if self.ignore(&line) {
312            return Ok(false);
313        }
314        self.insert(line);
315        Ok(true)
316    }
317
318    fn len(&self) -> usize {
319        self.entries.len()
320    }
321
322    fn is_empty(&self) -> bool {
323        self.entries.is_empty()
324    }
325
326    fn set_max_len(&mut self, len: usize) -> Result<()> {
327        self.max_len = len;
328        if self.len() > len {
329            self.entries.drain(..self.len() - len);
330        }
331        Ok(())
332    }
333
334    fn ignore_dups(&mut self, yes: bool) -> Result<()> {
335        self.ignore_dups = yes;
336        Ok(())
337    }
338
339    fn ignore_space(&mut self, yes: bool) {
340        self.ignore_space = yes;
341    }
342
343    fn save(&mut self, _: &Path) -> Result<()> {
344        unimplemented!();
345    }
346
347    fn append(&mut self, _: &Path) -> Result<()> {
348        unimplemented!();
349    }
350
351    fn load(&mut self, _: &Path) -> Result<()> {
352        unimplemented!();
353    }
354
355    fn clear(&mut self) -> Result<()> {
356        self.entries.clear();
357        Ok(())
358    }
359
360    fn search(
361        &self,
362        term: &str,
363        start: usize,
364        dir: SearchDirection,
365    ) -> Result<Option<SearchResult<'_>>> {
366        #[cfg(not(feature = "case_insensitive_history_search"))]
367        {
368            let test = |entry: &str| entry.find(term);
369            Ok(self.search_match(term, start, dir, test))
370        }
371        #[cfg(feature = "case_insensitive_history_search")]
372        {
373            use regex::{escape, RegexBuilder};
374            Ok(
375                if let Ok(re) = RegexBuilder::new(&escape(term))
376                    .case_insensitive(true)
377                    .build()
378                {
379                    let test = |entry: &str| re.find(entry).map(|m| m.start());
380                    self.search_match(term, start, dir, test)
381                } else {
382                    None
383                },
384            )
385        }
386    }
387
388    fn starts_with(
389        &self,
390        term: &str,
391        start: usize,
392        dir: SearchDirection,
393    ) -> Result<Option<SearchResult<'_>>> {
394        #[cfg(not(feature = "case_insensitive_history_search"))]
395        {
396            let test = |entry: &str| {
397                if entry.starts_with(term) {
398                    Some(term.len())
399                } else {
400                    None
401                }
402            };
403            Ok(self.search_match(term, start, dir, test))
404        }
405        #[cfg(feature = "case_insensitive_history_search")]
406        {
407            use regex::{escape, RegexBuilder};
408            Ok(
409                if let Ok(re) = RegexBuilder::new(&escape(term))
410                    .case_insensitive(true)
411                    .build()
412                {
413                    let test = |entry: &str| {
414                        re.find(entry)
415                            .and_then(|m| if m.start() == 0 { Some(m) } else { None })
416                            .map(|m| m.end())
417                    };
418                    self.search_match(term, start, dir, test)
419                } else {
420                    None
421                },
422            )
423        }
424    }
425}
426
427impl Index<usize> for MemHistory {
428    type Output = String;
429
430    fn index(&self, index: usize) -> &String {
431        &self.entries[index]
432    }
433}
434
435impl<'a> IntoIterator for &'a MemHistory {
436    type IntoIter = vec_deque::Iter<'a, String>;
437    type Item = &'a String;
438
439    fn into_iter(self) -> Self::IntoIter {
440        self.entries.iter()
441    }
442}
443
444/// Current state of the history stored in a file.
445#[derive(Default)]
446#[cfg(feature = "with-file-history")]
447pub struct FileHistory {
448    mem: MemHistory,
449    /// Number of entries inputted by user and not saved yet
450    new_entries: usize,
451    /// last path used by either `load` or `save`
452    path_info: Option<PathInfo>,
453}
454
455// TODO impl Deref<MemHistory> for FileHistory ?
456
457/// Last histo path, modified timestamp and size
458#[cfg(feature = "with-file-history")]
459struct PathInfo(std::path::PathBuf, SystemTime, usize);
460
461#[cfg(feature = "with-file-history")]
462impl FileHistory {
463    // New multiline-aware history files start with `#V2\n` and have newlines
464    // and backslashes escaped in them.
465    const FILE_VERSION_V2: &'static str = "#V2";
466
467    /// Default constructor
468    #[must_use]
469    pub fn new() -> Self {
470        Self::with_config(&Config::default())
471    }
472
473    /// Customized constructor with:
474    /// - [`Config::max_history_size()`],
475    /// - [`Config::history_ignore_space()`],
476    /// - [`Config::history_duplicates()`].
477    #[must_use]
478    pub fn with_config(config: &Config) -> Self {
479        Self {
480            mem: MemHistory::with_config(config),
481            new_entries: 0,
482            path_info: None,
483        }
484    }
485
486    fn save_to(&mut self, file: &File, append: bool) -> Result<()> {
487        use std::io::{BufWriter, Write as _};
488
489        fix_perm(file);
490        let mut wtr = BufWriter::new(file);
491        let first_new_entry = if append {
492            self.mem.len().saturating_sub(self.new_entries)
493        } else {
494            wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
495            wtr.write_all(b"\n")?;
496            0
497        };
498        for entry in self.mem.entries.iter().skip(first_new_entry) {
499            let mut bytes = entry.as_bytes();
500            while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) {
501                let (head, tail) = bytes.split_at(i);
502                wtr.write_all(head)?;
503
504                let (&escapable_byte, tail) = tail
505                    .split_first()
506                    .expect("memchr guarantees i is a valid index");
507                if escapable_byte == b'\n' {
508                    wtr.write_all(br"\n")?; // escaped line feed
509                } else {
510                    debug_assert_eq!(escapable_byte, b'\\');
511                    wtr.write_all(br"\\")?; // escaped backslash
512                }
513                bytes = tail;
514            }
515            wtr.write_all(bytes)?; // remaining bytes with no \n or \
516            wtr.write_all(b"\n")?;
517        }
518        // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485
519        wtr.flush()?;
520        Ok(())
521    }
522
523    fn load_from(&mut self, file: &File) -> Result<bool> {
524        use std::io::{BufRead as _, BufReader};
525
526        let rdr = BufReader::new(file);
527        let mut lines = rdr.lines();
528        let mut v2 = false;
529        if let Some(first) = lines.next() {
530            let line = first?;
531            if line == Self::FILE_VERSION_V2 {
532                v2 = true;
533            } else {
534                self.add_owned(line)?;
535            }
536        }
537        let mut appendable = v2;
538        for line in lines {
539            let mut line = line?;
540            if line.is_empty() {
541                continue;
542            }
543            if v2 {
544                let mut copy = None; // lazily copy line if unescaping is needed
545                let mut str = line.as_str();
546                while let Some(i) = str.find('\\') {
547                    if copy.is_none() {
548                        copy = Some(String::with_capacity(line.len()));
549                    }
550                    let s = copy.as_mut().unwrap();
551                    s.push_str(&str[..i]);
552                    let j = i + 1; // escaped char idx
553                    let b = if j < str.len() {
554                        str.as_bytes()[j]
555                    } else {
556                        0 // unexpected if History::save works properly
557                    };
558                    match b {
559                        b'n' => {
560                            s.push('\n'); // unescaped line feed
561                        }
562                        b'\\' => {
563                            s.push('\\'); // unescaped back slash
564                        }
565                        _ => {
566                            // only line feed and back slash should have been escaped
567                            warn!(target: "rustyline", "bad escaped line: {line}");
568                            copy = None;
569                            break;
570                        }
571                    }
572                    str = &str[j + 1..];
573                }
574                if let Some(mut s) = copy {
575                    s.push_str(str); // remaining bytes with no escaped char
576                    line = s;
577                }
578            }
579            appendable &= self.add_owned(line)?; // TODO truncate to MAX_LINE
580        }
581        self.new_entries = 0; // TODO we may lost new entries if loaded lines < max_len
582        Ok(appendable)
583    }
584
585    fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> {
586        let modified = file.metadata()?.modified()?;
587        if let Some(PathInfo(
588            ref mut previous_path,
589            ref mut previous_modified,
590            ref mut previous_size,
591        )) = self.path_info
592        {
593            if previous_path.as_path() != path {
594                path.clone_into(previous_path);
595            }
596            *previous_modified = modified;
597            *previous_size = size;
598        } else {
599            self.path_info = Some(PathInfo(path.to_owned(), modified, size));
600        }
601        debug!(target: "rustyline", "PathInfo({path:?}, {modified:?}, {size})");
602        Ok(())
603    }
604
605    fn can_just_append(&self, path: &Path, file: &File) -> Result<bool> {
606        if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) =
607            self.path_info
608        {
609            if previous_path.as_path() != path {
610                debug!(target: "rustyline", "cannot append: {previous_path:?} <> {path:?}");
611                return Ok(false);
612            }
613            let modified = file.metadata()?.modified()?;
614            if *previous_modified != modified
615                || self.mem.max_len <= *previous_size
616                || self.mem.max_len < (*previous_size).saturating_add(self.new_entries)
617            {
618                debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}",
619                       previous_modified, modified, self.mem.max_len, previous_size, self.new_entries);
620                Ok(false)
621            } else {
622                Ok(true)
623            }
624        } else {
625            Ok(false)
626        }
627    }
628
629    /// Return a forward iterator.
630    #[must_use]
631    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &String> + '_ {
632        self.mem.entries.iter()
633    }
634}
635
636/// Default transient in-memory history implementation
637#[cfg(not(feature = "with-file-history"))]
638pub type DefaultHistory = MemHistory;
639/// Default file-based history implementation
640#[cfg(feature = "with-file-history")]
641pub type DefaultHistory = FileHistory;
642
643#[cfg(feature = "with-file-history")]
644impl History for FileHistory {
645    fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult<'_>>> {
646        self.mem.get(index, dir)
647    }
648
649    fn add(&mut self, line: &str) -> Result<bool> {
650        if self.mem.add(line)? {
651            self.new_entries = self.new_entries.saturating_add(1).min(self.len());
652            Ok(true)
653        } else {
654            Ok(false)
655        }
656    }
657
658    fn add_owned(&mut self, line: String) -> Result<bool> {
659        if self.mem.add_owned(line)? {
660            self.new_entries = self.new_entries.saturating_add(1).min(self.len());
661            Ok(true)
662        } else {
663            Ok(false)
664        }
665    }
666
667    fn len(&self) -> usize {
668        self.mem.len()
669    }
670
671    fn is_empty(&self) -> bool {
672        self.mem.is_empty()
673    }
674
675    fn set_max_len(&mut self, len: usize) -> Result<()> {
676        self.mem.set_max_len(len)?;
677        self.new_entries = self.new_entries.min(len);
678        Ok(())
679    }
680
681    fn ignore_dups(&mut self, yes: bool) -> Result<()> {
682        self.mem.ignore_dups(yes)
683    }
684
685    fn ignore_space(&mut self, yes: bool) {
686        self.mem.ignore_space(yes);
687    }
688
689    fn save(&mut self, path: &Path) -> Result<()> {
690        if self.is_empty() || self.new_entries == 0 {
691            return Ok(());
692        }
693        let old_umask = umask();
694        let f = File::create(path);
695        restore_umask(old_umask);
696        let file = f?;
697        file.lock()?;
698        self.save_to(&file, false)?;
699        self.new_entries = 0;
700        self.update_path(path, &file, self.len())
701    }
702
703    fn append(&mut self, path: &Path) -> Result<()> {
704        use std::io::Seek as _;
705
706        if self.is_empty() || self.new_entries == 0 {
707            return Ok(());
708        }
709        if !path.exists() || self.new_entries == self.mem.max_len {
710            return self.save(path);
711        }
712        let mut file = OpenOptions::new().write(true).read(true).open(path)?;
713        file.lock()?;
714        if self.can_just_append(path, &file)? {
715            file.seek(SeekFrom::End(0))?;
716            self.save_to(&file, true)?;
717            let size = self
718                .path_info
719                .as_ref()
720                .unwrap()
721                .2
722                .saturating_add(self.new_entries);
723            self.new_entries = 0;
724            return self.update_path(path, &file, size);
725        }
726        // we may need to truncate file before appending new entries
727        let mut other = Self {
728            mem: MemHistory {
729                entries: VecDeque::new(),
730                max_len: self.mem.max_len,
731                ignore_space: self.mem.ignore_space,
732                ignore_dups: self.mem.ignore_dups,
733            },
734            new_entries: 0,
735            path_info: None,
736        };
737        other.load_from(&file)?;
738        let first_new_entry = self.mem.len().saturating_sub(self.new_entries);
739        for entry in self.mem.entries.iter().skip(first_new_entry) {
740            other.add(entry)?;
741        }
742        file.seek(SeekFrom::Start(0))?;
743        file.set_len(0)?; // if new size < old size
744        other.save_to(&file, false)?;
745        self.update_path(path, &file, other.len())?;
746        self.new_entries = 0;
747        Ok(())
748    }
749
750    fn load(&mut self, path: &Path) -> Result<()> {
751        let file = File::open(path)?;
752        file.lock_shared()?;
753        let len = self.len();
754        if self.load_from(&file)? {
755            self.update_path(path, &file, self.len() - len)
756        } else {
757            // discard old version on next save
758            self.path_info = None;
759            Ok(())
760        }
761    }
762
763    fn clear(&mut self) -> Result<()> {
764        self.mem.clear()?;
765        self.new_entries = 0;
766        Ok(())
767    }
768
769    fn search(
770        &self,
771        term: &str,
772        start: usize,
773        dir: SearchDirection,
774    ) -> Result<Option<SearchResult<'_>>> {
775        self.mem.search(term, start, dir)
776    }
777
778    fn starts_with(
779        &self,
780        term: &str,
781        start: usize,
782        dir: SearchDirection,
783    ) -> Result<Option<SearchResult<'_>>> {
784        self.mem.starts_with(term, start, dir)
785    }
786}
787
788#[cfg(feature = "with-file-history")]
789impl Index<usize> for FileHistory {
790    type Output = String;
791
792    fn index(&self, index: usize) -> &String {
793        &self.mem.entries[index]
794    }
795}
796
797#[cfg(feature = "with-file-history")]
798impl<'a> IntoIterator for &'a FileHistory {
799    type IntoIter = vec_deque::Iter<'a, String>;
800    type Item = &'a String;
801
802    fn into_iter(self) -> Self::IntoIter {
803        self.mem.entries.iter()
804    }
805}
806
807#[cfg(feature = "with-file-history")]
808cfg_if::cfg_if! {
809    if #[cfg(any(windows, target_arch = "wasm32"))] {
810        fn umask() -> u16 {
811            0
812        }
813
814        fn restore_umask(_: u16) {}
815
816        fn fix_perm(_: &File) {}
817    } else if #[cfg(unix)] {
818        use nix::sys::stat::{self, Mode, fchmod};
819        fn umask() -> Mode {
820            stat::umask(Mode::S_IXUSR | Mode::S_IRWXG | Mode::S_IRWXO)
821        }
822
823        fn restore_umask(old_umask: Mode) {
824            stat::umask(old_umask);
825        }
826
827        fn fix_perm(file: &File) {
828            let _ = fchmod(file, Mode::S_IRUSR | Mode::S_IWUSR);
829        }
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use super::{DefaultHistory, History as _, SearchDirection, SearchResult};
836    use crate::config::Config;
837    use crate::Result;
838
839    fn init() -> DefaultHistory {
840        let mut history = DefaultHistory::new();
841        assert!(history.add("line1").unwrap());
842        assert!(history.add("line2").unwrap());
843        assert!(history.add("line3").unwrap());
844        history
845    }
846
847    #[test]
848    fn new() {
849        let history = DefaultHistory::new();
850        assert_eq!(0, history.len());
851    }
852
853    #[test]
854    fn add() {
855        let config = Config::builder().history_ignore_space(true).build();
856        let mut history = DefaultHistory::with_config(&config);
857        #[cfg(feature = "with-file-history")]
858        assert_eq!(config.max_history_size(), history.mem.max_len);
859        assert!(history.add("line1").unwrap());
860        assert!(history.add("line2").unwrap());
861        assert!(!history.add("line2").unwrap());
862        assert!(!history.add("").unwrap());
863        assert!(!history.add(" line3").unwrap());
864    }
865
866    #[test]
867    fn set_max_len() {
868        let mut history = init();
869        history.set_max_len(1).unwrap();
870        assert_eq!(1, history.len());
871        assert_eq!(Some(&"line3".to_owned()), history.into_iter().last());
872    }
873
874    #[test]
875    #[cfg(feature = "with-file-history")]
876    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
877    fn save() -> Result<()> {
878        check_save("line\nfour \\ abc")
879    }
880
881    #[test]
882    #[cfg(feature = "with-file-history")]
883    #[cfg_attr(miri, ignore)] // unsupported operation: `open` not available when isolation is enabled
884    fn save_windows_path() -> Result<()> {
885        let path = "cd source\\repos\\forks\\nushell\\";
886        check_save(path)
887    }
888
889    #[cfg(feature = "with-file-history")]
890    fn check_save(line: &str) -> Result<()> {
891        let mut history = init();
892        assert!(history.add(line)?);
893        let tf = tempfile::NamedTempFile::new()?;
894
895        history.save(tf.path())?;
896        let mut history2 = DefaultHistory::new();
897        history2.load(tf.path())?;
898        for (a, b) in history.iter().zip(history2.iter()) {
899            assert_eq!(a, b);
900        }
901        tf.close()?;
902        Ok(())
903    }
904
905    #[test]
906    #[cfg(feature = "with-file-history")]
907    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
908    fn load_legacy() -> Result<()> {
909        use std::io::Write as _;
910        let tf = tempfile::NamedTempFile::new()?;
911        {
912            let mut legacy = std::fs::File::create(tf.path())?;
913            // Some data we'd accidentally corrupt if we got the version wrong
914            let data = b"\
915                test\\n \\abc \\123\n\
916                123\\n\\\\n\n\
917                abcde
918            ";
919            legacy.write_all(data)?;
920            legacy.flush()?;
921        }
922        let mut history = DefaultHistory::new();
923        history.load(tf.path())?;
924        assert_eq!(history[0], "test\\n \\abc \\123");
925        assert_eq!(history[1], "123\\n\\\\n");
926        assert_eq!(history[2], "abcde");
927
928        tf.close()?;
929        Ok(())
930    }
931
932    #[test]
933    #[cfg(feature = "with-file-history")]
934    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
935    fn append() -> Result<()> {
936        let mut history = init();
937        let tf = tempfile::NamedTempFile::new()?;
938
939        history.append(tf.path())?;
940
941        let mut history2 = DefaultHistory::new();
942        history2.load(tf.path())?;
943        history2.add("line4")?;
944        history2.append(tf.path())?;
945
946        history.add("line5")?;
947        history.append(tf.path())?;
948
949        let mut history3 = DefaultHistory::new();
950        history3.load(tf.path())?;
951        assert_eq!(history3.len(), 5);
952
953        tf.close()?;
954        Ok(())
955    }
956
957    #[test]
958    #[cfg(feature = "with-file-history")]
959    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
960    fn truncate() -> Result<()> {
961        let tf = tempfile::NamedTempFile::new()?;
962
963        let config = Config::builder().history_ignore_dups(false)?.build();
964        let mut history = DefaultHistory::with_config(&config);
965        history.add("line1")?;
966        history.add("line1")?;
967        history.append(tf.path())?;
968
969        let mut history = DefaultHistory::new();
970        history.load(tf.path())?;
971        history.add("l")?;
972        history.append(tf.path())?;
973
974        let mut history = DefaultHistory::new();
975        history.load(tf.path())?;
976        assert_eq!(history.len(), 2);
977        assert_eq!(history[1], "l");
978
979        tf.close()?;
980        Ok(())
981    }
982
983    #[test]
984    fn search() -> Result<()> {
985        let history = init();
986        assert_eq!(None, history.search("", 0, SearchDirection::Forward)?);
987        assert_eq!(None, history.search("none", 0, SearchDirection::Forward)?);
988        assert_eq!(None, history.search("line", 3, SearchDirection::Forward)?);
989
990        assert_eq!(
991            Some(SearchResult {
992                idx: 0,
993                entry: history.get(0, SearchDirection::Forward)?.unwrap().entry,
994                pos: 0
995            }),
996            history.search("line", 0, SearchDirection::Forward)?
997        );
998        assert_eq!(
999            Some(SearchResult {
1000                idx: 1,
1001                entry: history.get(1, SearchDirection::Forward)?.unwrap().entry,
1002                pos: 0
1003            }),
1004            history.search("line", 1, SearchDirection::Forward)?
1005        );
1006        assert_eq!(
1007            Some(SearchResult {
1008                idx: 2,
1009                entry: history.get(2, SearchDirection::Forward)?.unwrap().entry,
1010                pos: 0
1011            }),
1012            history.search("line3", 1, SearchDirection::Forward)?
1013        );
1014        Ok(())
1015    }
1016
1017    #[test]
1018    fn reverse_search() -> Result<()> {
1019        let history = init();
1020        assert_eq!(None, history.search("", 2, SearchDirection::Reverse)?);
1021        assert_eq!(None, history.search("none", 2, SearchDirection::Reverse)?);
1022        assert_eq!(None, history.search("line", 3, SearchDirection::Reverse)?);
1023
1024        assert_eq!(
1025            Some(SearchResult {
1026                idx: 2,
1027                entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry,
1028                pos: 0
1029            }),
1030            history.search("line", 2, SearchDirection::Reverse)?
1031        );
1032        assert_eq!(
1033            Some(SearchResult {
1034                idx: 1,
1035                entry: history.get(1, SearchDirection::Reverse)?.unwrap().entry,
1036                pos: 0
1037            }),
1038            history.search("line", 1, SearchDirection::Reverse)?
1039        );
1040        assert_eq!(
1041            Some(SearchResult {
1042                idx: 0,
1043                entry: history.get(0, SearchDirection::Reverse)?.unwrap().entry,
1044                pos: 0
1045            }),
1046            history.search("line1", 1, SearchDirection::Reverse)?
1047        );
1048        Ok(())
1049    }
1050
1051    #[test]
1052    #[cfg(feature = "case_insensitive_history_search")]
1053    fn anchored_search() -> Result<()> {
1054        let history = init();
1055        assert_eq!(
1056            Some(SearchResult {
1057                idx: 2,
1058                entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry,
1059                pos: 4
1060            }),
1061            history.starts_with("LiNe", 2, SearchDirection::Reverse)?
1062        );
1063        assert_eq!(
1064            None,
1065            history.starts_with("iNe", 2, SearchDirection::Reverse)?
1066        );
1067        Ok(())
1068    }
1069}