use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Mutex;
pub const PAGE_BYTES: u64 = 8 * 1024 * 1024;
static FLOOR_POS: AtomicI64 = AtomicI64::new(0);
static FLOOR_NUM: AtomicI64 = AtomicI64::new(0);
static ARMED: AtomicBool = AtomicBool::new(false);
static HIST_PATH: Mutex<String> = Mutex::new(String::new());
static PAGE_LOCK: Mutex<()> = Mutex::new(());
pub fn count_entries(path: &str) -> Option<u64> {
let f = std::fs::File::open(path).ok()?;
let len = f.metadata().ok()?.len();
if len == 0 {
return Some(0);
}
let map = unsafe { memmap2::Mmap::map(&f).ok()? };
let bytes: &[u8] = ↦
let extended = bytes.starts_with(b": ")
|| bytes.windows(3).take(65536).any(|w| w == b"\n: ");
let mut count: u64 = 0;
let mut prev_cont = false; let mut line_start = 0usize;
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'\n' {
let line = &bytes[line_start..i];
if !prev_cont {
if extended {
if line.starts_with(b": ") {
count += 1;
}
} else if !line.is_empty() {
count += 1;
}
}
prev_cont = line.ends_with(b"\\");
line_start = i + 1;
}
i += 1;
}
if line_start < bytes.len() && !prev_cont {
let line = &bytes[line_start..];
if (extended && line.starts_with(b": ")) || (!extended && !line.is_empty()) {
count += 1;
}
}
Some(count)
}
pub fn arm(path: &str, floor_pos: i64, floor_num: i64) {
*HIST_PATH.lock().unwrap() = path.to_string();
FLOOR_POS.store(floor_pos, Ordering::SeqCst);
FLOOR_NUM.store(floor_num, Ordering::SeqCst);
ARMED.store(floor_pos > 0, Ordering::SeqCst);
tracing::info!(
path,
floor_pos,
floor_num,
"history: lazy paging armed (newest page loaded)"
);
}
pub fn disarm() {
ARMED.store(false, Ordering::SeqCst);
FLOOR_POS.store(0, Ordering::SeqCst);
}
pub fn has_older() -> bool {
ARMED.load(Ordering::SeqCst) && FLOOR_POS.load(Ordering::SeqCst) > 0
}
pub fn floor_num() -> i64 {
FLOOR_NUM.load(Ordering::SeqCst)
}
pub fn page_older_until(min_ev: i64) -> bool {
if !has_older() {
return false;
}
let _g = PAGE_LOCK.lock().unwrap();
if !has_older() {
return false;
}
let path = HIST_PATH.lock().unwrap().clone();
let mut loaded_any = false;
while has_older() && (min_ev <= 0 || FLOOR_NUM.load(Ordering::SeqCst) > min_ev) {
let floor_pos = FLOOR_POS.load(Ordering::SeqCst) as u64;
let start = floor_pos.saturating_sub(PAGE_BYTES);
let (mut entries, aligned_start) = match read_entries_between(&path, start, floor_pos) {
Some(v) => v,
None => {
disarm();
break;
}
};
if entries.is_empty() {
if start == 0 {
disarm();
break;
}
FLOOR_POS.store(start as i64, Ordering::SeqCst);
continue;
}
let mut num = FLOOR_NUM.load(Ordering::SeqCst);
for e in entries.iter_mut().rev() {
num -= 1;
e.histnum = num;
}
let new_floor_num = num;
{
let mut ring = crate::ported::hist::hist_ring.lock().unwrap();
for e in entries.into_iter().rev() {
ring.push(e);
}
}
FLOOR_NUM.store(new_floor_num, Ordering::SeqCst);
FLOOR_POS.store(aligned_start as i64, Ordering::SeqCst);
if aligned_start == 0 {
disarm();
}
loaded_any = true;
if new_floor_num <= 1 {
disarm();
}
}
loaded_any
}
fn read_entries_between(path: &str, start: u64, end: u64) -> Option<(Vec<crate::ported::zsh_h::histent>, u64)> {
use std::io::{Read, Seek, SeekFrom};
if end <= start {
return Some((Vec::new(), start));
}
let mut f = std::fs::File::open(path).ok()?;
f.seek(SeekFrom::Start(start)).ok()?;
let mut bytes = vec![0u8; (end - start) as usize];
f.read_exact(&mut bytes).ok()?;
crate::ported::utils::unmetafy(&mut bytes);
let text = String::from_utf8_lossy(&bytes);
let extended = text.starts_with(": ") || text.contains("\n: ");
let mut offset = 0u64;
let mut lines: Vec<&str> = Vec::new();
{
let mut rest = &text[..];
if start > 0 {
match rest.find('\n') {
Some(i) => {
offset = (i + 1) as u64;
rest = &rest[i + 1..];
}
None => return Some((Vec::new(), start)),
}
loop {
let line_end = rest.find('\n').unwrap_or(rest.len());
let line = &rest[..line_end];
let is_head = if extended {
line.starts_with(": ") && line.contains(';')
} else {
true
};
if is_head || rest.is_empty() {
break;
}
let adv = line_end + if line_end < rest.len() { 1 } else { 0 };
offset += adv as u64;
rest = &rest[adv..];
if rest.is_empty() {
return Some((Vec::new(), start + offset));
}
}
}
lines.extend(rest.lines());
}
let mut out: Vec<crate::ported::zsh_h::histent> = Vec::new();
let mut current: Option<(i64, i64, String)> = None;
let flush = |cur: &mut Option<(i64, i64, String)>,
out: &mut Vec<crate::ported::zsh_h::histent>| {
if let Some((stim, ftim, text)) = cur.take() {
out.push(crate::ported::zsh_h::histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: text,
flags: crate::ported::zsh_h::HIST_OLD as i32,
},
up: None,
down: None,
zle_text: None,
stim,
ftim,
words: Vec::new(),
nwords: 0,
histnum: 0, });
}
};
for raw_line in lines {
if let Some((_, _, ref mut text)) = current {
if text.ends_with('\\') {
text.pop();
text.push('\n');
text.push_str(raw_line);
continue;
}
flush(&mut current, &mut out);
}
if let Some(rest) = raw_line.strip_prefix(": ") {
if let Some((meta, text)) = rest.split_once(';') {
if let Some((stim_s, dur_s)) = meta.split_once(':') {
let stim: i64 = stim_s.parse().unwrap_or(0);
let dur: i64 = dur_s.parse().unwrap_or(0);
current = Some((stim, stim + dur, text.to_string()));
continue;
}
}
}
current = Some((0, 0, raw_line.to_string()));
}
flush(&mut current, &mut out);
Some((out, start + offset))
}