blob: f9460c20db3d9d619fd2c2eade4d994635e8667f [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Provides history functionality for crosh commands.
use std::collections::VecDeque;
use std::fmt::{self, Display};
use std::fs::File;
use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::str::{from_utf8, Utf8Error};
use remain::sorted;
const DEFAULT_MAX_ENTRIES: usize = 100;
const MAX_HISTORY_SIZE: u64 = 4096;
#[sorted]
pub enum Error {
IO(io::Error),
UTF8(Utf8Error),
}
impl Display for Error {
#[remain::check]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
#[sorted]
match self {
IO(err) => write!(f, "IOError: {}", err),
UTF8(err) => write!(f, "UTF8Error: {}", err),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::IO(err)
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Self {
Error::UTF8(err)
}
}
pub struct History {
temp: String, // A slot for a command that hasn't been entered, but not issued.
index: isize, // Track the position in the queue.
max_entries: usize,
commands: VecDeque<String>,
}
impl History {
pub fn new() -> History {
History::with_max_capacity(DEFAULT_MAX_ENTRIES)
}
pub fn load_from_file(&mut self, path: &Path) -> Result<(), Error> {
let mut file = File::open(path)?;
// Grab the last 4kb of the file (or all if it is smaller).
let file_size = file.seek(SeekFrom::End(0))?;
let data_size = if file_size > MAX_HISTORY_SIZE {
file.seek(SeekFrom::End(-(MAX_HISTORY_SIZE as i64)))?;
MAX_HISTORY_SIZE as usize
} else {
file.seek(SeekFrom::Start(0))?;
file_size as usize
};
let mut raw_data = vec![0; data_size];
file.read_exact(&mut raw_data)?;
// Find a good starting index if the data wasn't taken from the beginning of the file.
let start: usize = if file_size < MAX_HISTORY_SIZE {
0
} else {
let (i, _) = raw_data
.iter()
.enumerate()
.find(|(_, c)| **c == b'\n')
.unwrap_or((data_size, &b' '));
i
};
// Convert to UTF-8 and split by lines.
let temp = from_utf8(&raw_data[start..])?;
let mut entries: Vec<&str> = temp.lines().filter(|s| !s.is_empty()).collect();
// Add entries to the back of the history until it is full.
while self.commands.len() < self.max_entries && !entries.is_empty() {
self.commands.push_front(entries.pop().unwrap().to_string());
}
Ok(())
}
pub fn persist_to_file(&self, path: &Path) -> Result<(), io::Error> {
let mut writer = BufWriter::new(File::create(path)?);
for cmd in &self.commands {
writeln!(writer, "{}", &cmd)?;
}
Ok(())
}
pub fn with_max_capacity(c: usize) -> History {
History {
temp: String::new(),
index: -1,
max_entries: c,
commands: VecDeque::new(),
}
}
pub fn new_entry(&mut self, entry: String) {
self.temp.clear();
self.index = -1;
// Do not add entries that begin with a space.
if entry.starts_with(' ') {
return;
}
// Dedupe sequential commands.
if Some(&entry) == self.commands.front() {
return;
}
while self.commands.len() >= self.max_entries {
self.commands.pop_front();
}
self.commands.push_back(entry);
}
pub fn previous(&mut self, current: &str) -> Option<&str> {
// Backup the command the user hasn't executed yet.
if self.index == -1 {
self.temp = current.to_string();
}
if (self.index + 1) as usize == self.commands.len() {
return None;
}
self.index += 1;
self.get_entry(self.index as usize)
}
pub fn next(&mut self) -> Option<&str> {
// Make sure we aren't already at the bottom.
if self.index == -1 {
return None;
}
self.index -= 1;
// If we reached the bottom, restore the command that hasn't been submitted yet.
if self.index == -1 {
return Some(&self.temp);
}
// This should always have something to return.
Some(self.get_entry(self.index as usize).unwrap())
}
fn get_entry(&self, reverse_index: usize) -> Option<&str> {
self.commands
.get(self.commands.len() - reverse_index - 1)
.map(|s| &**s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::read_to_string;
use tempfile::TempDir;
const TEST_MAX_ENTRIES: usize = 3;
#[test]
fn test_history_with_capacity() {
let history = History::with_max_capacity(TEST_MAX_ENTRIES);
assert_eq!(history.max_entries, TEST_MAX_ENTRIES);
}
#[test]
fn test_history_new_entry_bounds() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
for i in 0..(TEST_MAX_ENTRIES + 1) {
history.new_entry(format!("entry {}", i));
}
assert_eq!(history.commands.front(), Some(&"entry 1".to_string()));
assert_eq!(
history.commands.back(),
Some(&format!("entry {}", TEST_MAX_ENTRIES).to_string())
);
}
#[test]
fn test_load_from_file_small_success() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let tempdir = TempDir::new().unwrap();
let test_file_name = tempdir.path().join("test_file");
let mut test_file = File::create(test_file_name.as_path()).unwrap();
test_file
.write_all(b"entry 1\nentry 2\nentry 3\n\nentry 4")
.unwrap();
drop(test_file);
assert!(history.load_from_file(test_file_name.as_path()).is_ok());
assert_eq!(history.commands.len(), 3);
for i in 4..2 {
assert_eq!(history.previous(""), Some(&**&format!("entry {}", i)));
}
}
#[test]
fn test_load_from_file_big_success() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let tempdir = TempDir::new().unwrap();
let test_file_name = tempdir.path().join("test_file");
let mut test_file = File::create(test_file_name.as_path()).unwrap();
// 0x80 is invalid UTF-8 and would trigger an error if it was not skipped correctly.
test_file.write(&[0x80; MAX_HISTORY_SIZE as usize]).unwrap();
test_file.write_all(b"entry 0\nentry 1\nentry 2").unwrap();
drop(test_file);
assert!(history.load_from_file(test_file_name.as_path()).is_ok());
assert_eq!(history.commands.len(), 2);
for i in 2..1 {
assert_eq!(history.previous(""), Some(&**&format!("entry {}", i)));
}
}
#[test]
fn test_load_from_file_utf8error() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let tempdir = TempDir::new().unwrap();
let test_file_name = tempdir.path().join("test_file");
let mut test_file = File::create(test_file_name.as_path()).unwrap();
test_file.write(&[0x80; 1]).unwrap();
test_file
.write_all(b"entry 1\nentry 2\nentry 3\n\nentry 4")
.unwrap();
drop(test_file);
if let Err(Error::UTF8(_)) = history.load_from_file(test_file_name.as_path()) {
} else {
panic!();
}
}
#[test]
fn test_load_from_file_ioerror() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
if let Err(Error::IO(_)) = history.load_from_file(Path::new("/dev/null/does_not_exist")) {
} else {
panic!();
}
}
#[test]
fn test_persist_to_file_success() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let tempdir = TempDir::new().unwrap();
let test_file_name = tempdir.path().join("test_file");
for i in 0..TEST_MAX_ENTRIES {
history.new_entry(format!("entry {}", i));
}
assert!(history.persist_to_file(test_file_name.as_path()).is_ok());
assert_eq!(
read_to_string(test_file_name.as_path()).unwrap(),
"entry 0\nentry 1\nentry 2\n"
);
}
#[test]
fn test_persist_to_file_ioerror() {
let history = History::with_max_capacity(TEST_MAX_ENTRIES);
assert!(history
.persist_to_file(Path::new("/dev/null/does_not_exist"))
.is_err());
}
#[test]
fn test_history_new_entry_ignore() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
history.new_entry(" entry".to_string());
assert!(history.commands.is_empty());
}
#[test]
fn test_history_new_entry_dedupe() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let entry: &str = "entry";
history.new_entry(entry.to_string());
history.new_entry(entry.to_string());
assert_eq!(history.commands.len(), 1);
}
#[test]
fn test_history_new_entry_index_reset() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let first_entry = "entry 0";
history.new_entry(first_entry.to_string());
assert_eq!(history.previous(""), Some(first_entry));
let second_entry = "entry 1";
history.new_entry(second_entry.to_string());
assert_eq!(history.previous(""), Some(second_entry));
}
#[test]
fn test_history_previous_bounds() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
let command = "temp";
for i in 0..TEST_MAX_ENTRIES {
history.new_entry(format!("entry {}", i));
}
for i in 0..TEST_MAX_ENTRIES {
assert_eq!(
history.previous(command),
Some(&**&format!("entry {}", TEST_MAX_ENTRIES - 1 - i))
);
}
assert_eq!(history.previous(command), None);
}
#[test]
fn test_history_next_bounds() {
let mut history = History::with_max_capacity(TEST_MAX_ENTRIES);
for i in 0..TEST_MAX_ENTRIES {
history.new_entry(format!("entry {}", i));
}
let command = "temp";
history.previous(command);
// This is one greater than possible, but it simplifies the test.
history.index = history.commands.len() as isize;
for i in 0..TEST_MAX_ENTRIES {
assert_eq!(history.next(), Some(&**&format!("entry {}", i)));
}
assert_eq!(history.next(), Some(command));
assert_eq!(history.next(), None);
}
}