rustyline/
history.rs

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