1use super::{
2 base::CommandLineSearch, History, HistoryItem, HistoryItemId, SearchDirection, SearchQuery,
3};
4use crate::{
5 result::{ReedlineError, ReedlineErrorVariants},
6 HistorySessionId, Result,
7};
8
9use std::{
10 collections::VecDeque,
11 fs::OpenOptions,
12 io::{BufRead, BufReader, BufWriter, Seek, SeekFrom, Write},
13 ops::{Deref, DerefMut},
14 path::PathBuf,
15};
16
17pub const HISTORY_SIZE: usize = 1000;
19pub const NEWLINE_ESCAPE: &str = "<\\n>";
20
21#[derive(Debug)]
28pub struct FileBackedHistory {
29 capacity: usize,
30 entries: VecDeque<String>,
31 file: Option<PathBuf>,
32 len_on_disk: usize, session: Option<HistorySessionId>,
34}
35
36impl Default for FileBackedHistory {
37 fn default() -> Self {
41 Self::new(HISTORY_SIZE)
42 }
43}
44
45fn encode_entry(s: &str) -> String {
46 s.replace('\n', NEWLINE_ESCAPE)
47}
48
49fn decode_entry(s: &str) -> String {
50 s.replace(NEWLINE_ESCAPE, "\n")
51}
52
53impl History for FileBackedHistory {
54 fn save(&mut self, h: HistoryItem) -> Result<HistoryItem> {
56 let entry = h.command_line;
57 let entry_id = if self
59 .entries
60 .back()
61 .map_or(true, |previous| previous != &entry)
62 && !entry.is_empty()
63 {
64 if self.entries.len() == self.capacity {
65 self.entries.pop_front();
68 self.len_on_disk = self.len_on_disk.saturating_sub(1);
69 }
70 self.entries.push_back(entry.to_string());
71 Some(HistoryItemId::new((self.entries.len() - 1) as i64))
72 } else {
73 None
74 };
75 Ok(FileBackedHistory::construct_entry(entry_id, entry))
76 }
77
78 fn load(&self, id: HistoryItemId) -> Result<super::HistoryItem> {
79 Ok(FileBackedHistory::construct_entry(
80 Some(id),
81 self.entries
82 .get(id.0 as usize)
83 .ok_or(ReedlineError(ReedlineErrorVariants::OtherHistoryError(
84 "Item does not exist",
85 )))?
86 .clone(),
87 ))
88 }
89
90 fn count(&self, query: SearchQuery) -> Result<i64> {
91 Ok(self.search(query)?.len() as i64)
93 }
94
95 fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>> {
96 if query.start_time.is_some() || query.end_time.is_some() {
97 return Err(ReedlineError(
98 ReedlineErrorVariants::HistoryFeatureUnsupported {
99 history: "FileBackedHistory",
100 feature: "filtering by time",
101 },
102 ));
103 }
104
105 if query.filter.hostname.is_some()
106 || query.filter.cwd_exact.is_some()
107 || query.filter.cwd_prefix.is_some()
108 || query.filter.exit_successful.is_some()
109 {
110 return Err(ReedlineError(
111 ReedlineErrorVariants::HistoryFeatureUnsupported {
112 history: "FileBackedHistory",
113 feature: "filtering by extra info",
114 },
115 ));
116 }
117 let (min_id, max_id) = {
118 let start = query.start_id.map(|e| e.0);
119 let end = query.end_id.map(|e| e.0);
120 if let SearchDirection::Backward = query.direction {
121 (end, start)
122 } else {
123 (start, end)
124 }
125 };
126 let min_id = min_id.map(|e| e + 1).unwrap_or(0);
128 let max_id = max_id
130 .map(|e| e - 1)
131 .unwrap_or(self.entries.len() as i64 - 1);
132 if max_id < 0 || min_id > self.entries.len() as i64 - 1 {
133 return Ok(vec![]);
134 }
135 let intrinsic_limit = max_id - min_id + 1;
136 let limit = if let Some(given_limit) = query.limit {
137 std::cmp::min(intrinsic_limit, given_limit) as usize
138 } else {
139 intrinsic_limit as usize
140 };
141 let filter = |(idx, cmd): (usize, &String)| {
142 if !match &query.filter.command_line {
143 Some(CommandLineSearch::Prefix(p)) => cmd.starts_with(p),
144 Some(CommandLineSearch::Substring(p)) => cmd.contains(p),
145 Some(CommandLineSearch::Exact(p)) => cmd == p,
146 None => true,
147 } {
148 return None;
149 }
150 if let Some(str) = &query.filter.not_command_line {
151 if cmd == str {
152 return None;
153 }
154 }
155 Some(FileBackedHistory::construct_entry(
156 Some(HistoryItemId::new(idx as i64)),
157 cmd.to_string(), ))
159 };
160
161 let iter = self
162 .entries
163 .iter()
164 .enumerate()
165 .skip(min_id as usize)
166 .take(intrinsic_limit as usize);
167 if let SearchDirection::Backward = query.direction {
168 Ok(iter.rev().filter_map(filter).take(limit).collect())
169 } else {
170 Ok(iter.filter_map(filter).take(limit).collect())
171 }
172 }
173
174 fn update(
175 &mut self,
176 _id: super::HistoryItemId,
177 _updater: &dyn Fn(super::HistoryItem) -> super::HistoryItem,
178 ) -> Result<()> {
179 Err(ReedlineError(
180 ReedlineErrorVariants::HistoryFeatureUnsupported {
181 history: "FileBackedHistory",
182 feature: "updating entries",
183 },
184 ))
185 }
186
187 fn clear(&mut self) -> Result<()> {
188 self.entries.clear();
189 self.len_on_disk = 0;
190
191 if let Some(file) = &self.file {
192 if let Err(err) = std::fs::remove_file(file) {
193 return Err(ReedlineError(ReedlineErrorVariants::IOError(err)));
194 }
195 }
196
197 Ok(())
198 }
199
200 fn delete(&mut self, _h: super::HistoryItemId) -> Result<()> {
201 Err(ReedlineError(
202 ReedlineErrorVariants::HistoryFeatureUnsupported {
203 history: "FileBackedHistory",
204 feature: "removing entries",
205 },
206 ))
207 }
208
209 fn sync(&mut self) -> std::io::Result<()> {
213 if let Some(fname) = &self.file {
214 let own_entries = self.entries.range(self.len_on_disk..);
216
217 if let Some(base_dir) = fname.parent() {
218 std::fs::create_dir_all(base_dir)?;
219 }
220
221 let mut f_lock = fd_lock::RwLock::new(
222 OpenOptions::new()
223 .create(true)
224 .write(true)
225 .read(true)
226 .open(fname)?,
227 );
228 let mut writer_guard = f_lock.write()?;
229 let (mut foreign_entries, truncate) = {
230 let reader = BufReader::new(writer_guard.deref());
231 let mut from_file = reader
232 .lines()
233 .map(|o| o.map(|i| decode_entry(&i)))
234 .collect::<std::io::Result<VecDeque<_>>>()?;
235 if from_file.len() + own_entries.len() > self.capacity {
236 (
237 from_file.split_off(from_file.len() - (self.capacity - own_entries.len())),
238 true,
239 )
240 } else {
241 (from_file, false)
242 }
243 };
244
245 {
246 let mut writer = BufWriter::new(writer_guard.deref_mut());
247 if truncate {
248 writer.rewind()?;
249
250 for line in &foreign_entries {
251 writer.write_all(encode_entry(line).as_bytes())?;
252 writer.write_all("\n".as_bytes())?;
253 }
254 } else {
255 writer.seek(SeekFrom::End(0))?;
256 }
257 for line in own_entries {
258 writer.write_all(encode_entry(line).as_bytes())?;
259 writer.write_all("\n".as_bytes())?;
260 }
261 writer.flush()?;
262 }
263 if truncate {
264 let file = writer_guard.deref_mut();
265 let file_len = file.stream_position()?;
266 file.set_len(file_len)?;
267 }
268
269 let own_entries = self.entries.drain(self.len_on_disk..);
270 foreign_entries.extend(own_entries);
271 self.entries = foreign_entries;
272
273 self.len_on_disk = self.entries.len();
274 }
275 Ok(())
276 }
277
278 fn session(&self) -> Option<HistorySessionId> {
279 self.session
280 }
281}
282
283impl FileBackedHistory {
284 pub fn new(capacity: usize) -> Self {
290 if capacity == usize::MAX {
291 panic!("History capacity too large to be addressed safely");
292 }
293 FileBackedHistory {
294 capacity,
295 entries: VecDeque::new(),
296 file: None,
297 len_on_disk: 0,
298 session: None,
299 }
300 }
301
302 pub fn with_file(capacity: usize, file: PathBuf) -> std::io::Result<Self> {
311 let mut hist = Self::new(capacity);
312 if let Some(base_dir) = file.parent() {
313 std::fs::create_dir_all(base_dir)?;
314 }
315 hist.file = Some(file);
316 hist.sync()?;
317 Ok(hist)
318 }
319
320 fn construct_entry(id: Option<HistoryItemId>, command_line: String) -> HistoryItem {
322 HistoryItem {
323 id,
324 start_timestamp: None,
325 command_line,
326 session_id: None,
327 hostname: None,
328 cwd: None,
329 duration: None,
330 exit_status: None,
331 more_info: None,
332 }
333 }
334}
335
336impl Drop for FileBackedHistory {
337 fn drop(&mut self) {
339 let _res = self.sync();
340 }
341}