use std::fs::File;
use std::io::{BufWriter, Read, Write};
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::collections::HashSet;
use std::collections::HashMap;
use crossbeam_channel::Receiver;
use flate2::read::GzDecoder;
use liblzma::read::XzDecoder;
use zstd::stream::read::Decoder as ZstdDecoder;
use memmap2::Mmap;
use color_eyre::eyre::{Result, Context};
use log::warn;
use regex::bytes::RegexBuilder;
use crate::models::*;
use crate::lfs;
use serde_json;
fn find_all_matches(haystack: &[u8], pattern: &[u8], ignore_case: bool) -> Vec<usize> {
if ignore_case {
let lower_haystack = haystack.to_ascii_lowercase();
memchr::memmem::Finder::new(pattern).find_iter(&lower_haystack).collect()
} else {
memchr::memmem::Finder::new(pattern).find_iter(haystack).collect()
}
}
fn find_first_match(haystack: &[u8], pattern: &[u8], ignore_case: bool) -> Option<usize> {
if ignore_case {
let lower_haystack = haystack.to_ascii_lowercase();
memchr::memmem::Finder::new(pattern).find(&lower_haystack)
} else {
memchr::memmem::Finder::new(pattern).find(haystack)
}
}
#[allow(dead_code)]
fn find_last_match(haystack: &[u8], pattern: &[u8], ignore_case: bool) -> Option<usize> {
if ignore_case {
let lower_haystack = haystack.to_ascii_lowercase();
memchr::memmem::rfind(&lower_haystack, pattern)
} else {
memchr::memmem::rfind(haystack, pattern)
}
}
fn is_glob_pattern(s: &str) -> bool {
s.contains('*') || s.contains('?') || s.contains('[')
}
#[derive(Debug, Default, Clone)]
pub struct SearchOptions {
pub ignore_case: bool,
pub files: bool,
pub paths: bool,
pub regexp: bool,
pub glob: bool,
pub origin_pattern: String,
pub u8_literal: Vec<u8>,
pub regex_pattern: Option<regex::bytes::Regex>,
pub glob_pattern: Option<glob::Pattern>,
pub collected_results: Option<Arc<Mutex<Vec<(String, String)>>>>,
pub in_fields: Vec<String>,
pub format: Option<String>,
pub limit: Option<usize>,
pub result_count: Option<Arc<Mutex<usize>>>,
}
fn setup_patterns(options: &mut SearchOptions) -> Result<()> {
let mut literal_pattern = options.origin_pattern.clone();
if options.regexp {
let mut regex_builder = RegexBuilder::new(&options.origin_pattern);
let regex = regex_builder.case_insensitive(options.ignore_case).build()?;
options.regex_pattern = Some(regex);
}
if !options.regexp && is_glob_pattern(&options.origin_pattern) {
options.glob = true;
options.glob_pattern = Some(glob::Pattern::new(&options.origin_pattern).unwrap());
}
if options.regexp || options.glob {
if let Some(literal) = crate::search::extract_literal_string(&options.origin_pattern) {
literal_pattern = literal;
} else {
log::warn!("Failed to extract literal, cannot handle complex regexp now");
}
}
if options.ignore_case {
literal_pattern = literal_pattern.to_lowercase();
}
options.u8_literal = literal_pattern.as_bytes().to_vec();
if (channel_config().format == crate::models::PackageFormat::Deb ||
channel_config().format == crate::models::PackageFormat::Pacman) &&
!options.u8_literal.is_empty() &&
options.u8_literal[0] == b'/' {
options.u8_literal.remove(0);
}
log::debug!("setup_patterns: {:?}", options);
Ok(())
}
pub fn search_repo_cache(options: &mut SearchOptions) -> Result<()> {
crate::repo::sync_channel_metadata()?;
let repodata_indice = repodata_indice();
let mut any_filelists = false;
let mut consumer_handles = Vec::new();
let mut producer_handles = Vec::new();
setup_patterns(options)?;
if options.limit.is_some() {
options.result_count = Some(Arc::new(Mutex::new(0)));
}
for repo_index in repodata_indice.values() {
let repo_dir = PathBuf::from(&repo_index.repo_dir_path);
for shard in repo_index.repo_shards.values() {
if options.files || options.paths {
if let Some(filelists) = &shard.filelists {
let filelists_path = repo_dir.join(&filelists.filename);
if lfs::exists_on_host(&filelists_path) {
let (consumer_handle, producer_handle) = search_filelists(filelists_path, options)
.with_context(|| format!("Failed to search filelists in {}", repo_index.repodata_name))?;
consumer_handles.push(consumer_handle);
producer_handles.push(producer_handle);
any_filelists = true;
} else {
warn!("Filelists not found at {}", filelists_path.display());
}
}
} else {
let filename = shard.packages.filename.clone();
search_packages(&repo_dir.join(&filename), options)
.with_context(|| format!("Failed to search package info in {}", repo_index.repodata_name))?;
}
}
}
if !any_filelists && (options.files || options.paths) {
match channel_config().format {
PackageFormat::Apk => {
eprintln!("Alpine has no filelists for search");
std::process::exit(0);
}
PackageFormat::Conda => {
eprintln!("Conda has no filelists for search");
std::process::exit(0);
}
PackageFormat::Brew => {
eprintln!("Homebrew has no filelists for search");
std::process::exit(0);
}
_ => {
eprintln!("No filelists downloaded yet, please run 'epkg update' first");
std::process::exit(0);
}
}
}
for handle in producer_handles {
handle.join().unwrap()?;
}
for handle in consumer_handles {
handle.join().unwrap()?;
}
Ok(())
}
pub fn search_filelists(filelists_path: PathBuf, options: &mut SearchOptions) -> Result<(thread::JoinHandle<Result<()>>, thread::JoinHandle<Result<()>>)> {
let (tx, rx) = crossbeam_channel::bounded::<Arc<Mutex<FixedBuffer>>>(1);
let buffer_pool = Arc::new(SharedBufferPool::new(BUFFER_COUNT, BUFFER_SIZE));
let options_arc = Arc::new(options.clone());
let producer_buffer_pool = Arc::clone(&buffer_pool);
let producer_handle = start_filelists_producer(filelists_path.clone(), tx, producer_buffer_pool);
let consumer_buffer_pool = Arc::clone(&buffer_pool);
let is_rpm_xml = filelists_path.to_str().unwrap_or("").contains(".xml");
let consumer_handle = thread::spawn(move || {
let options = &*options_arc;
if is_rpm_xml {
process_rpm_filelists(rx, options, consumer_buffer_pool)
} else {
process_simple_filelists(rx, options, consumer_buffer_pool)
}
});
Ok((consumer_handle, producer_handle))
}
struct FixedBuffer {
data: Vec<u8>,
used: usize,
}
impl FixedBuffer {
fn new(capacity: usize) -> Self {
FixedBuffer {
data: vec![0; capacity],
used: 0,
}
}
fn clear(&mut self) {
self.used = 0;
}
fn as_slice(&self) -> &[u8] {
&self.data[0..self.used]
}
fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.data
}
fn available_space(&self) -> usize {
self.data.len() - self.used
}
fn set_used(&mut self, size: usize) {
assert!(size <= self.data.len());
self.used = size;
}
fn nr_used(&mut self) -> usize {
self.used
}
fn copy_from_slice(&mut self, src: &[u8], start_pos: usize) {
let end_pos = start_pos + src.len();
assert!(end_pos <= self.data.len(), "Buffer overflow");
self.data[start_pos..end_pos].copy_from_slice(src);
self.used = self.used.max(end_pos);
}
fn copy_at_start(&mut self, src: &[u8]) {
self.copy_from_slice(src, 0);
}
}
const BUFFER_SIZE: usize = 128 * 1024;
const BUFFER_COUNT: usize = 4;
struct SharedBufferPool {
buffers: Vec<Arc<Mutex<FixedBuffer>>>,
producer_idx: AtomicUsize,
#[allow(dead_code)]
consumer_idx: AtomicUsize,
#[allow(dead_code)]
buffer_count: usize,
}
impl SharedBufferPool {
fn new(buffer_count: usize, buffer_size: usize) -> Self {
assert!(buffer_count >= 4, "Buffer count must be at least 4");
let mut buffers = Vec::with_capacity(buffer_count);
for _ in 0..buffer_count {
buffers.push(Arc::new(Mutex::new(FixedBuffer::new(buffer_size))));
}
SharedBufferPool {
buffers,
producer_idx: AtomicUsize::new(0),
consumer_idx: AtomicUsize::new(0),
buffer_count,
}
}
fn get_producer_buffer(&self) -> Arc<Mutex<FixedBuffer>> {
let idx = self.producer_idx.load(Ordering::SeqCst);
Arc::clone(&self.buffers[idx % self.buffers.len()])
}
fn get_next_producer_buffer(&self) -> Arc<Mutex<FixedBuffer>> {
let idx = self.producer_idx.load(Ordering::SeqCst);
Arc::clone(&self.buffers[(idx + 1) % self.buffers.len()])
}
fn advance_producer(&self) {
let current = self.producer_idx.load(Ordering::SeqCst);
let next = (current + 1) % self.buffers.len();
self.producer_idx.store(next, Ordering::SeqCst);
}
}
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ ZERO-COPY CIRCULAR BUFFER DESIGN │
* └───────────────────────────────────────────────────────────────────────────┘
*
* This implementation uses a circular buffer pool with advancing indices to achieve
* true zero-copy data flow between producer and consumer threads. The design
* eliminates unnecessary memory copies while maintaining thread safety.
*
* ┌─────────┬─────────┬─────────┬─────────┐
* │ Buffer0 │ Buffer1 │ Buffer2 │ Buffer3 │
* └─────────┴─────────┴─────────┴─────────┘
* ^ ^
* consumer_idx producer_idx
*
* Key features:
*
* 1. CIRCULAR BUFFER MECHANICS:
* - Both producer and consumer threads access all buffers in the pool
* - They maintain separate indices that advance through the buffer pool
* - consumer_idx is always 1 behind producer_idx (modulo buffer count)
* - Atomic operations ensure thread-safe index advancement
*
* 2. BUFFER ROLES BASED ON RELATIVE POSITION:
* - producer_idx: Current buffer being filled by producer
* - producer_idx+1: Next buffer for producer (for partial lines)
* - producer_idx-1: Pending in channel
* - consumer_idx: Current buffer being processed by consumer
*
* 3. ZERO-COPY DATA FLOW:
* - Producer fills a buffer and advances its index
* - Consumer processes the same buffer when its index reaches it
* - No copying between producer and consumer buffers
* - Minimal copying only for combining partial XML elements
*
* 4. SYNCHRONIZATION & BACKPRESSURE:
* - Producer waits if it would overwrite a buffer still in use by consumer
* - Consumer waits if producer hasn't filled the next buffer yet
* - Natural backpressure prevents memory exhaustion
*
* 5. MEMORY EFFICIENCY:
* - Fixed number of pre-allocated buffers (BUFFER_COUNT)
* - Fixed buffer size (BUFFER_SIZE) bounds memory usage
* - Reuse of buffers eliminates allocation/deallocation overhead
*
* This design significantly reduces memory pressure and improves throughput
* by eliminating unnecessary copies while maintaining thread safety through
* careful coordination of buffer access between threads.
*/
fn rfind_byte(data: &[u8], byte: u8) -> Option<usize> {
if data.is_empty() {
return None;
}
for i in (0..data.len()).rev() {
if data[i] == byte {
return Some(i);
}
}
None
}
fn open_filelists_reader(filelists_path: &Path) -> Result<Box<dyn Read>> {
let file = File::open(filelists_path)?;
let s = filelists_path.to_string_lossy();
Ok(if s.ends_with(".gz") {
Box::new(GzDecoder::new(file))
} else if s.ends_with(".xz") {
Box::new(XzDecoder::new_parallel(file))
} else if s.ends_with(".zst") {
Box::new(ZstdDecoder::new(file)?)
} else {
Box::new(file)
})
}
fn start_filelists_producer(
filelists_path: PathBuf,
tx: crossbeam_channel::Sender<Arc<Mutex<FixedBuffer>>>,
buffer_pool: Arc<SharedBufferPool>
) -> thread::JoinHandle<Result<()>> {
thread::spawn(move || {
let mut current_buffer = buffer_pool.get_producer_buffer();
let mut partial_size = 0;
let mut reader = open_filelists_reader(&filelists_path)?;
loop {
let mut locked_buffer = current_buffer.lock().unwrap();
if partial_size == 0 {
locked_buffer.clear();
}
let available = locked_buffer.available_space();
if available == 0 {
drop(locked_buffer);
if tx.send(Arc::clone(¤t_buffer)).is_err() {
return Ok(());
}
buffer_pool.advance_producer();
current_buffer = buffer_pool.get_producer_buffer();
partial_size = 0;
continue;
}
let buffer_slice = locked_buffer.as_mut_slice();
let bytes_read = reader.read(&mut buffer_slice[partial_size..])?;
if bytes_read > 0 {
locked_buffer.set_used(partial_size + bytes_read);
}
if bytes_read == 0 {
if locked_buffer.nr_used() > 0 {
drop(locked_buffer);
if tx.send(Arc::clone(¤t_buffer)).is_err() {
return Ok(());
}
} else {
drop(locked_buffer);
}
break;
}
let data = locked_buffer.as_slice();
if let Some(boundary) = rfind_byte(data, b'\n') {
let boundary = boundary + 1;
let new_partial_size = if boundary < data.len() {
data.len() - boundary
} else {
0
};
if new_partial_size > 0 {
let next_buffer = buffer_pool.get_next_producer_buffer();
let mut next_locked = next_buffer.lock().unwrap();
next_locked.clear();
next_locked.copy_at_start(&data[boundary..]);
next_locked.set_used(new_partial_size);
locked_buffer.set_used(boundary);
drop(next_locked);
}
drop(locked_buffer);
if tx.send(Arc::clone(¤t_buffer)).is_err() {
return Ok(());
}
buffer_pool.advance_producer();
current_buffer = buffer_pool.get_producer_buffer();
partial_size = new_partial_size;
} else {
partial_size = locked_buffer.nr_used();
drop(locked_buffer);
if partial_size > BUFFER_SIZE - 1024 {
if tx.send(Arc::clone(¤t_buffer)).is_err() {
return Ok(());
}
buffer_pool.advance_producer();
current_buffer = buffer_pool.get_producer_buffer();
partial_size = 0;
}
}
}
Ok(())
})
}
fn find_line_boundaries(data: &[u8], start_pos: usize, end_pos: usize) -> (usize, usize) {
let line_start = if start_pos == 0 {
0
} else {
memchr::memrchr(b'\n', &data[..start_pos])
.map(|pos| pos + 1)
.unwrap_or(0)
};
let line_end = find_next_newline(data, end_pos);
(line_start, line_end)
}
fn process_simple_filelists(
rx: Receiver<Arc<Mutex<FixedBuffer>>>,
options: &SearchOptions,
_buffer_pool: Arc<SharedBufferPool>
) -> Result<()> {
while let Ok(arc_chunk) = rx.recv() {
let mut chunk_guard = arc_chunk.lock().unwrap();
let chunk_data = chunk_guard.as_slice();
let mut processed_lines = HashSet::new();
for match_pos in find_all_matches(chunk_data, &options.u8_literal, options.ignore_case) {
let (line_start, line_end) = find_line_boundaries(chunk_data, match_pos, match_pos + 1);
if processed_lines.contains(&line_start) {
continue;
}
processed_lines.insert(line_start);
let line = &chunk_data[line_start..line_end];
process_simple_line(line, options)?;
}
chunk_guard.clear();
drop(chunk_guard);
}
Ok(())
}
fn process_simple_line(
line: &[u8],
options: &SearchOptions
) -> Result<()> {
if let Some(space_pos) = memchr::memchr(b' ', line) {
let (pkgname, path) = match channel_config().format {
PackageFormat::Deb => {
let path = &line[..space_pos];
let mut pkgname_start = space_pos + 1;
while pkgname_start < line.len() && line[pkgname_start] == b' ' {
pkgname_start += 1;
}
let pkgname = &line[pkgname_start..];
(pkgname, path)
},
_ => {
let pkgname = &line[..space_pos];
let path = &line[space_pos + 1..];
(pkgname, path)
}
};
let mut abs_path_vec = Vec::with_capacity(1 + path.len());
abs_path_vec.push(b'/');
abs_path_vec.extend_from_slice(path);
let indeed_match = check_match_path(&abs_path_vec, options);
if indeed_match {
print_path(pkgname, &abs_path_vec, options);
}
}
Ok(())
}
fn print_path(pkgname: &[u8], path: &[u8], options: &SearchOptions) {
if let (Ok(pkg_str), Ok(path_str)) = (std::str::from_utf8(pkgname), std::str::from_utf8(path)) {
if let Some(ref results) = options.collected_results {
let mut results = results.lock().unwrap();
results.push((pkg_str.to_string(), path_str.to_string()));
} else {
println!("{} {}", pkg_str, path_str);
}
}
}
fn check_match_path(path: &[u8], options: &SearchOptions) -> bool {
if options.files {
if let Some(mut fname_pos) = memchr::memrchr(b'/', path) {
if !options.u8_literal.is_empty() && options.u8_literal[0] != b'/' {
fname_pos += 1;
} else {
}
let filename = &path[fname_pos..];
match_pattern(filename, options)
} else {
match_pattern(path, options)
}
} else {
match_pattern(path, options)
}
}
fn match_pattern(content: &[u8], options: &SearchOptions) -> bool {
if let Some(regex) = &options.regex_pattern {
return regex.is_match(content);
}
if let Some(pattern) = &options.glob_pattern {
let content_str = String::from_utf8_lossy(content);
let content_str = if options.ignore_case {
content_str.to_lowercase()
} else {
content_str.into_owned()
};
return pattern.matches(&content_str);
}
find_first_match(content, &options.u8_literal, options.ignore_case).is_some()
}
fn process_rpm_filelists(
rx: Receiver<Arc<Mutex<FixedBuffer>>>,
options: &SearchOptions,
_buffer_pool: Arc<SharedBufferPool>
) -> Result<()> {
let mut current_pkgname = Vec::<u8>::new();
while let Ok(arc_chunk) = rx.recv() {
let mut chunk_guard = arc_chunk.lock().unwrap();
{
let chunk_data = chunk_guard.as_slice();
process_rpm_filelists_with_memmem(&mut current_pkgname, chunk_data, options)?;
}
chunk_guard.clear();
drop(chunk_guard);
}
Ok(())
}
<package pkgid="e01a85beb0abfbb377f060882d281d3052e0cbadf77d67c9ff1d4533c42f0d17" name="CUnit" arch="x86_64">
<version epoch="0" ver="2.1.3" rel="24.oe2403"/>
<file>/etc/ima/digest_lists.tlv/0-metadata_list-compact_tlv-CUnit-2.1.3-24.oe2403.x86_64</file>
<file>/etc/ima/digest_lists/0-metadata_list-compact-CUnit-2.1.3-24.oe2403.x86_64</file>
<file>/usr/lib64/libcunit.so.1</file>
<file>/usr/lib64/libcunit.so.1.0.1</file>
<file type="dir">/usr/share/CUnit</file>
<file>/usr/share/CUnit/CUnit-List.dtd</file>
<file>/usr/share/CUnit/CUnit-List.xsl</file>
*/
fn process_rpm_filelists_with_memmem(current_pkgname: &mut Vec<u8>, chunk_data: &[u8], options: &SearchOptions) -> Result<()> {
let mut processed_lines = HashSet::new();
for match_pos in find_all_matches(chunk_data, &options.u8_literal, options.ignore_case) {
let (line_start, line_end) = find_line_boundaries(chunk_data, match_pos, match_pos + 1);
if processed_lines.contains(&line_start) {
continue;
}
processed_lines.insert(line_start);
let line = &chunk_data[line_start..line_end];
let file_path = if let Some(rest) = line.strip_prefix(b" <file>") {
if let Some(rest) = rest.strip_suffix(b"</file>") {
rest
} else {
line
}
} else if line.starts_with(b" <file type=\"") {
if let Some(rest) = line.strip_suffix(b"</file>") {
if let Some(quote_pos) = memchr::memchr(b'"', &line[14..]) {
let content_start = 14 + quote_pos + 2;
if content_start < rest.len() {
&rest[content_start..]
} else {
line
}
} else {
line
}
} else {
line
}
} else {
continue;
};
let indeed_match = check_match_path(file_path, options);
if indeed_match {
if let Some(pkg_name) = rfind_pkgname_in_xml(&chunk_data[0..line_start]) {
*current_pkgname = pkg_name.into();
}
print_path(current_pkgname, file_path, options);
}
}
if let Some(pkg_name) = rfind_pkgname_in_xml(chunk_data) {
*current_pkgname = pkg_name.into();
}
Ok(())
}
fn rfind_pkgname_in_xml(data: &[u8]) -> Option<String> {
static NAME_STR: &[u8] = b"name";
static QUOTE: u8 = b'\"';
static EQUAL_SIGN: u8 = b'=';
let mut pos = data.len();
while pos > 0 {
if let Some(eq_pos) = memchr::memrchr(EQUAL_SIGN, &data[..pos]) {
if eq_pos >= NAME_STR.len() &&
&data[eq_pos - NAME_STR.len()..eq_pos] == NAME_STR &&
eq_pos + 1 < data.len() &&
data[eq_pos + 1] == QUOTE {
let name_start = eq_pos + 2;
if let Some(quote_pos) = memchr::memchr(QUOTE, &data[name_start..]) {
if let Ok(name) = std::str::from_utf8(&data[name_start..(name_start + quote_pos)]) {
return Some(name.to_string());
}
}
}
pos = eq_pos;
} else {
break;
}
}
None
}
#[allow(dead_code)]
struct PackagesSearchState<'a> {
current_pkgname: &'a [u8],
current_summary: &'a [u8],
stdout: BufWriter<std::io::Stdout>,
}
#[allow(dead_code)]
impl<'a> PackagesSearchState<'a> {
fn new() -> Self {
PackagesSearchState {
current_pkgname: &b""[..],
current_summary: &b""[..],
stdout: BufWriter::new(std::io::stdout()),
}
}
fn print_match(&mut self) -> Result<()> {
writeln!(
self.stdout,
"{} - {}",
String::from_utf8_lossy(self.current_pkgname),
String::from_utf8_lossy(self.current_summary)
)?;
Ok(())
}
}
pub fn extract_literal_string(pattern: &str) -> Option<String> {
let special_chars = ['.', '*', '+', '?', '|', '^', '$', '\\'];
// Track nesting level of parentheses and brackets
let mut paren_level = 0;
let mut bracket_level = 0;
let mut brace_level = 0;
// Track the current and longest literal sequences
let mut current_literal = String::new();
let mut longest_literal = String::new();
// Process each character in the pattern
for c in pattern.chars() {
match c {
'(' => {
paren_level += 1;
if paren_level == 1 && !current_literal.is_empty() {
// Save the current literal if it's longer than what we have
if current_literal.len() > longest_literal.len() {
longest_literal = current_literal.clone();
}
current_literal.clear();
}
},
')' => {
if paren_level > 0 {
paren_level -= 1;
}
},
'[' => {
bracket_level += 1;
if bracket_level == 1 && !current_literal.is_empty() {
if current_literal.len() > longest_literal.len() {
longest_literal = current_literal.clone();
}
current_literal.clear();
}
},
']' => {
if bracket_level > 0 {
bracket_level -= 1;
}
},
'{' => {
brace_level += 1;
if brace_level == 1 && !current_literal.is_empty() {
if current_literal.len() > longest_literal.len() {
longest_literal = current_literal.clone();
}
current_literal.clear();
}
},
'}' => {
if brace_level > 0 {
brace_level -= 1;
}
},
_ if paren_level == 0 && bracket_level == 0 && brace_level == 0 => {
if special_chars.contains(&c) {
if !current_literal.is_empty() {
if current_literal.len() > longest_literal.len() {
longest_literal = current_literal.clone();
}
current_literal.clear();
}
} else {
current_literal.push(c);
}
},
_ => {}
}
}
if !current_literal.is_empty() && current_literal.len() > longest_literal.len() {
longest_literal = current_literal;
}
if longest_literal.is_empty() {
None
} else {
Some(longest_literal)
}
}
#[inline]
fn find_next_newline(data: &[u8], start: usize) -> usize {
memchr::memchr(b'\n', &data[start..])
.map(|pos| start + pos)
.unwrap_or(data.len())
}
static PKGNAME_PATTERN: &[u8] = b"pkgname: ";
static SUMMARY_PATTERN: &[u8] = b"summary: ";
fn is_line_start(data: &[u8], pos: usize) -> bool {
pos == 0 || data[pos - 1] == b'\n'
}
fn find_and_extract_pattern(data: &[u8], search_end: usize, pattern: &[u8], search_backwards: bool) -> Option<(Vec<u8>, usize)> {
let pos = if search_backwards {
memchr::memmem::rfind(&data[..search_end], pattern)
} else {
memchr::memmem::find(&data[search_end..], pattern).map(|p| search_end + p)
};
if let Some(pos) = pos {
if is_line_start(data, pos) {
let value_start = pos + pattern.len();
let value_end = find_next_newline(data, value_start);
let mut value = Vec::new();
value.extend_from_slice(&data[value_start..value_end]);
return Some((value, pos));
}
return Some((Vec::new(), pos));
}
None
}
fn search_package_metadata(
chunk: &[u8],
search_end: usize,
current_pkgname: &mut Vec<u8>,
current_summary: &mut Vec<u8>,
) -> (bool, bool) {
let mut found_pkgname = false;
let mut found_summary = false;
if let Some((pkg_value, pkg_pos)) = find_and_extract_pattern(chunk, search_end, PKGNAME_PATTERN, true) {
if !pkg_value.is_empty() {
current_pkgname.clear();
current_pkgname.extend_from_slice(&pkg_value);
found_pkgname = true;
if let Some((sum_value, _)) = find_and_extract_pattern(chunk, pkg_pos, SUMMARY_PATTERN, false) {
if !sum_value.is_empty() {
current_summary.clear();
current_summary.extend_from_slice(&sum_value);
found_summary = true;
}
}
}
}
(found_pkgname, found_summary)
}
fn find_paragraph_boundaries(data: &[u8], match_pos: usize) -> (usize, usize) {
let paragraph_start = if match_pos == 0 {
0
} else {
let mut pos = match_pos;
while pos > 0 {
if data[pos - 1] == b'\n' {
if pos > 1 && data[pos - 2] == b'\n' {
pos = pos - 1;
break;
}
}
pos -= 1;
}
pos
};
let paragraph_end = {
let mut pos = match_pos;
while pos < data.len() {
if data[pos] == b'\n' {
if pos + 1 < data.len() && data[pos + 1] == b'\n' {
break;
}
}
pos += 1;
}
pos
};
(paragraph_start, paragraph_end)
}
fn parse_paragraph_to_hashmap(paragraph: &[u8]) -> HashMap<String, String> {
let mut fields = HashMap::new();
let paragraph_str = String::from_utf8_lossy(paragraph);
let mut current_key = String::new();
let mut current_value = String::new();
for line in paragraph_str.lines() {
if let Some((key, value)) = line.split_once(": ") {
if !current_key.is_empty() {
fields.insert(current_key.clone(), current_value.clone());
}
current_key = key.trim().to_string();
current_value = value.trim().to_string();
} else if line.starts_with(' ') && !current_key.is_empty() {
current_value.push('\n');
current_value.push_str(line.trim());
}
}
if !current_key.is_empty() {
fields.insert(current_key, current_value);
}
fields
}
fn check_match_in_fields(fields: &HashMap<String, String>, options: &SearchOptions) -> bool {
if options.in_fields.is_empty() {
for value in fields.values() {
let value_bytes = value.as_bytes();
if match_pattern(value_bytes, options) {
return true;
}
}
return false;
}
for field_name in &options.in_fields {
if let Some(value) = fields.get(field_name) {
let value_bytes = value.as_bytes();
if match_pattern(value_bytes, options) {
return true;
}
}
}
false
}
fn format_package_output(fields: &HashMap<String, String>, format: &Option<String>) -> String {
match format {
Some(fmt) if fmt == "json" => {
serde_json::to_string(fields).unwrap_or_else(|e| {
log::warn!("Failed to serialize to JSON: {}", e);
"{}".to_string()
})
}
Some(fmt) => {
let mut result = fmt.clone();
loop {
let start = result.find("${");
if start.is_none() {
break;
}
let start = start.unwrap();
let end = result[start..].find('}');
if end.is_none() {
break;
}
let end = start + end.unwrap();
let field_spec = &result[start + 2..end];
let (field_name, width) = if let Some(semi_pos) = field_spec.find(';') {
let field = &field_spec[..semi_pos];
let width_str = &field_spec[semi_pos + 1..];
let width: i32 = width_str.parse().unwrap_or(0);
(field, width)
} else {
(field_spec, 0)
};
let value = fields.get(field_name).cloned().unwrap_or_default();
let formatted = if width > 0 {
format!("{:>width$}", value, width = width as usize)
} else if width < 0 {
format!("{:<width$}", value, width = (-width) as usize)
} else {
value
};
result.replace_range(start..end + 1, &formatted);
}
result = result.replace("\\t", "\t").replace("\\n", "\n");
result
}
None => {
let pkgname = fields.get("pkgname").cloned().unwrap_or_default();
let summary = fields.get("summary").cloned().unwrap_or_default();
format!("{} - {}", pkgname, summary)
}
}
}
fn search_packages_hashmap(mmap: &Mmap, options: &SearchOptions) -> Result<()> {
let mut stdout = BufWriter::new(std::io::stdout());
let mut printed_packages: HashSet<String> = HashSet::new();
let mut pos = 0;
while pos < mmap.len() {
if let Some(ref global_count) = options.result_count {
let count = global_count.lock().unwrap();
if *count >= options.limit.unwrap() {
return Ok(());
}
}
if let Some(relative_pos) = find_first_match(&mmap[pos..], &options.u8_literal, options.ignore_case) {
let pattern_pos = pos + relative_pos;
let (line_start, line_end) = find_line_boundaries(mmap, pattern_pos, pattern_pos + 1);
let line = &mmap[line_start..line_end];
if match_pattern(line, options) {
let (paragraph_start, paragraph_end) = find_paragraph_boundaries(mmap, pattern_pos);
let paragraph = &mmap[paragraph_start..paragraph_end];
let fields = parse_paragraph_to_hashmap(paragraph);
if !options.in_fields.is_empty() && !check_match_in_fields(&fields, options) {
pos = paragraph_end + 1;
continue;
}
let pkgname = fields.get("pkgname").cloned().unwrap_or_default();
let version = fields.get("version").cloned().unwrap_or_default();
let key = format!("{}-{}", pkgname, version);
if printed_packages.contains(&key) {
pos = paragraph_end + 1;
continue;
}
printed_packages.insert(key);
let output = format_package_output(&fields, &options.format);
writeln!(stdout, "{}", output)?;
if let Some(ref global_count) = options.result_count {
let mut count = global_count.lock().unwrap();
*count += 1;
if *count >= options.limit.unwrap() {
return Ok(());
}
}
pos = paragraph_end + 1;
continue;
}
pos = line_end + 1;
} else {
break;
}
}
Ok(())
}
fn search_packages_default(mmap: &Mmap, options: &SearchOptions) -> Result<()> {
let mut stdout = BufWriter::new(std::io::stdout());
let mut printed_packages: HashSet<String> = HashSet::new();
let mut current_pkgname = Vec::new();
let mut current_summary = Vec::new();
let mut pos = 0;
while pos < mmap.len() {
if let Some(relative_pos) = find_first_match(&mmap[pos..], &options.u8_literal, options.ignore_case) {
let pattern_pos = pos + relative_pos;
let (line_start, line_end) = find_line_boundaries(mmap, pattern_pos, pattern_pos + 1);
let line = &mmap[line_start..line_end];
if match_pattern(line, options) {
let (found_pkgname, found_summary) = search_package_metadata(
mmap,
line_start,
&mut current_pkgname,
&mut current_summary
);
if found_pkgname && found_summary {
let pkgname = String::from_utf8_lossy(¤t_pkgname);
if printed_packages.contains(&pkgname.to_string()) {
pos = line_end + 1;
continue;
}
printed_packages.insert(pkgname.to_string());
writeln!(stdout, "{} - {}", pkgname, String::from_utf8_lossy(¤t_summary))?;
}
pos = line_end + 1;
continue;
}
pos = line_end + 1;
} else {
break;
}
}
Ok(())
}
pub fn search_packages(packages_path: &Path, options: &SearchOptions) -> Result<()> {
let file = File::open(packages_path)?;
let mmap = unsafe { Mmap::map(&file)? };
let need_hashmap = !options.in_fields.is_empty() ||
options.format.is_some() ||
options.limit.is_some();
if need_hashmap {
search_packages_hashmap(&mmap, options)
} else {
search_packages_default(&mmap, options)
}
}