// Bound candidate retention while scanning potentially enormous rotation folders. // The heap's top is the least desirable result: oldest timestamp, then // lexicographically last name. Final output preserves newest/name order. fn newest_log_files( candidates: impl Iterator, limit: usize, ) -> Vec<(String, std::path::PathBuf, u64)> { use std::{cmp::Reverse, collections::BinaryHeap}; if limit == 0 { return Vec::new(); } let mut bounded = BinaryHeap::with_capacity(limit); for (name, path, modified_ms) in candidates { let item = (Reverse(modified_ms), name, path); if bounded.len() < limit { bounded.push(item); } else if bounded.peek().is_some_and(|worst| item.cmp(worst).is_lt()) { bounded.pop(); bounded.push(item); } } let mut result = bounded.into_iter() .map(|(Reverse(modified), name, path)| (name, path, modified)) .collect::>(); result.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0))); result } // Bounded file-log inventory helpers and gzip tail reader. fn rotated_gzip_sources( active: &std::path::Path, id_prefix: &str, label_prefix: &str, kind: &'static str, enabled: bool, format: &'static str, ) -> Vec { let Some(parent) = active.parent() else { return Vec::new(); }; let Some(base) = active.file_name().and_then(|name| name.to_str()) else { return Vec::new(); }; let safe = std::fs::symlink_metadata(parent).ok().is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink()); if !safe { return Vec::new(); } let prefix = format!("{base}."); let entries = newest_log_files(std::fs::read_dir(parent).ok().into_iter().flat_map(|iter| iter.filter_map(|entry| entry.ok())).filter_map(|entry| { let name = entry.file_name().to_string_lossy().into_owned(); if name.len() > 180 || !name.starts_with(&prefix) || !name.ends_with(".gz") { return None; } let file_type = entry.file_type().ok()?; if !file_type.is_file() || file_type.is_symlink() { return None; } let meta = entry.metadata().ok()?; Some((name, entry.path(), file_modified_ms(&meta))) }), 64); entries.into_iter().map(|(name,path,_)| source_from_path( format!("{id_prefix}:gz:{name}"), format!("{label_prefix} ยท {name}"), id_prefix.to_owned(), kind, path, enabled, format )).collect() } fn open_file_log(path: &std::path::Path) -> std::io::Result { let mut options = std::fs::OpenOptions::new(); options.read(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.custom_flags(libc::O_NOFOLLOW); } let file = options.open(path)?; let metadata = file.metadata()?; if !metadata.file_type().is_file() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "configured log path is not a regular file", )); } Ok(file) } fn read_tail( source: &FileLogSource, budget: u64, line_budget: usize, ) -> std::io::Result<(Vec, u64, bool)> { use std::io::{Read, Seek, SeekFrom}; let mut file = open_file_log(&source.path)?; let length = file.metadata()?.len(); if source.compressed { // gzip streams are not seekable. Read a complete bounded rotation, then // keep only the same 2 MiB decompressed tail used for plain files. if length > budget || length > FILE_LOG_GZIP_INPUT_MAX { return Ok((Vec::new(), length.min(budget), true)); } let mut decoder = flate2::read::GzDecoder::new(file); let mut decoded = Vec::new(); decoder .by_ref() .take(FILE_LOG_GZIP_OUTPUT_MAX + 1) .read_to_end(&mut decoded)?; if decoded.len() as u64 > FILE_LOG_GZIP_OUTPUT_MAX { return Ok((Vec::new(), length, true)); } let keep = decoded.len().min(FILE_LOG_SCAN_BYTES_PER_SOURCE as usize); let start = decoded.len().saturating_sub(keep); let mut content = decoded[start..].to_vec(); let truncated = start > 0; if truncated { if let Some(index) = content.iter().position(|byte| *byte == b'\n') { content.drain(..=index); } else { content.clear(); } } let text = String::from_utf8_lossy(&content); let mut iter = text.lines().rev(); let lines = iter.by_ref().take(line_budget).map(str::to_owned).collect::>(); return Ok((lines, length, truncated || iter.next().is_some())); } let bytes = length.min(budget).min(FILE_LOG_SCAN_BYTES_PER_SOURCE); let start = length.saturating_sub(bytes); file.seek(SeekFrom::Start(start))?; let mut content = Vec::with_capacity(bytes as usize); file.take(bytes).read_to_end(&mut content)?; let truncated = start > 0; if truncated { if let Some(index) = content.iter().position(|byte| *byte == b'\n') { content.drain(..=index); } else { content.clear(); } } let text = String::from_utf8_lossy(&content); let mut iter = text.lines().rev(); let lines = iter.by_ref().take(line_budget).map(str::to_owned).collect::>(); Ok((lines, bytes, truncated || iter.next().is_some())) }