9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
492 lines
14 KiB
Rust
492 lines
14 KiB
Rust
use std::io::{BufRead, BufReader};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
const MAX_LINE_BYTES: usize = 1024 * 1024; // 1MB per line — skip files with longer lines (likely binary)
|
|
const MAX_OUTPUT_BYTES: usize = 50 * 1024;
|
|
const MAX_OUTPUT_LINES: usize = 2000;
|
|
const MAX_CONTEXT: usize = 100;
|
|
const MAX_WALK_DEPTH: usize = 50;
|
|
|
|
pub fn run(args: Vec<String>) -> i32 {
|
|
if let Some(code) = try_system_rg(&args) {
|
|
return code;
|
|
}
|
|
fallback(args)
|
|
}
|
|
|
|
fn try_system_rg(args: &[String]) -> Option<i32> {
|
|
let self_exe = std::env::current_exe().ok()?;
|
|
let self_canon = std::fs::canonicalize(&self_exe).ok()?;
|
|
let cleaned_path = clean_path(&self_canon);
|
|
let candidate = which_rg(&cleaned_path)?;
|
|
|
|
let mut cmd = Command::new(&candidate);
|
|
cmd.args(args).env("PATH", &cleaned_path);
|
|
crate::configure_no_window(&mut cmd);
|
|
let status = cmd.status().ok()?;
|
|
Some(status.code().unwrap_or(2))
|
|
}
|
|
|
|
fn clean_path(self_canon: &Path) -> String {
|
|
let original = std::env::var("PATH").unwrap_or_default();
|
|
original
|
|
.split(':')
|
|
.filter(|dir| {
|
|
if dir.is_empty() {
|
|
return false;
|
|
}
|
|
let candidate = Path::new(dir).join("rg");
|
|
match std::fs::canonicalize(&candidate) {
|
|
Ok(c) => c != *self_canon,
|
|
Err(_) => true,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(":")
|
|
}
|
|
|
|
fn which_rg(path: &str) -> Option<PathBuf> {
|
|
for dir in path.split(':') {
|
|
if dir.is_empty() {
|
|
continue;
|
|
}
|
|
let candidate = Path::new(dir).join("rg");
|
|
if candidate.is_file() && is_executable(&candidate) {
|
|
return Some(candidate);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn is_executable(p: &Path) -> bool {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::metadata(p)
|
|
.map(|m| m.permissions().mode() & 0o111 != 0)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn is_executable(_p: &Path) -> bool {
|
|
true
|
|
}
|
|
|
|
#[cfg_attr(test, derive(Debug))]
|
|
struct RgArgs {
|
|
pattern: Option<String>,
|
|
paths: Vec<PathBuf>,
|
|
line_numbers: bool,
|
|
ignore_case: bool,
|
|
files_only: bool,
|
|
list_files_with_matches: bool,
|
|
context: usize,
|
|
glob: Option<String>,
|
|
}
|
|
|
|
fn parse(args: Vec<String>) -> Result<RgArgs, String> {
|
|
let mut out = RgArgs {
|
|
pattern: None,
|
|
paths: Vec::new(),
|
|
line_numbers: false,
|
|
ignore_case: false,
|
|
files_only: false,
|
|
list_files_with_matches: false,
|
|
context: 0,
|
|
glob: None,
|
|
};
|
|
let mut iter = args.into_iter();
|
|
let mut positional: Vec<String> = Vec::new();
|
|
while let Some(a) = iter.next() {
|
|
match a.as_str() {
|
|
"--files" => out.files_only = true,
|
|
"-n" | "--line-number" => out.line_numbers = true,
|
|
"-i" | "--ignore-case" => out.ignore_case = true,
|
|
"-l" | "--files-with-matches" => out.list_files_with_matches = true,
|
|
"-C" | "--context" => {
|
|
let n = iter.next().ok_or("missing value for -C")?;
|
|
let parsed: usize = n.parse().map_err(|_| format!("bad -C value: {n}"))?;
|
|
out.context = parsed.min(MAX_CONTEXT);
|
|
}
|
|
"-g" | "--glob" => {
|
|
out.glob = Some(iter.next().ok_or("missing value for -g")?);
|
|
}
|
|
"--" => positional.extend(iter.by_ref()),
|
|
s if s.starts_with('-') && s.len() > 1 => {
|
|
return Err(format!("unsupported flag (fallback rg): {s}"));
|
|
}
|
|
_ => positional.push(a),
|
|
}
|
|
}
|
|
if out.files_only {
|
|
out.paths = positional.into_iter().map(PathBuf::from).collect();
|
|
if out.paths.is_empty() {
|
|
out.paths.push(PathBuf::from("."));
|
|
}
|
|
} else {
|
|
let mut it = positional.into_iter();
|
|
out.pattern = it.next();
|
|
out.paths = it.map(PathBuf::from).collect();
|
|
if out.paths.is_empty() {
|
|
out.paths.push(PathBuf::from("."));
|
|
}
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
struct CappedSink {
|
|
bytes: usize,
|
|
lines: usize,
|
|
capped: bool,
|
|
}
|
|
|
|
impl CappedSink {
|
|
fn new() -> Self {
|
|
Self {
|
|
bytes: 0,
|
|
lines: 0,
|
|
capped: false,
|
|
}
|
|
}
|
|
|
|
fn writeln(&mut self, s: &str) {
|
|
if self.capped {
|
|
return;
|
|
}
|
|
let next = self.bytes.saturating_add(s.len()).saturating_add(1);
|
|
if next > MAX_OUTPUT_BYTES || self.lines >= MAX_OUTPUT_LINES {
|
|
self.capped = true;
|
|
tracing::warn!("rg (fallback): output capped at {MAX_OUTPUT_BYTES} bytes / {MAX_OUTPUT_LINES} lines");
|
|
return;
|
|
}
|
|
println!("{s}");
|
|
self.bytes = next;
|
|
self.lines += 1;
|
|
}
|
|
}
|
|
|
|
fn fallback(args: Vec<String>) -> i32 {
|
|
let opts = match parse(args) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
tracing::error!("rg (fallback): {e}");
|
|
return 2;
|
|
}
|
|
};
|
|
let mut sink = CappedSink::new();
|
|
let mut found = false;
|
|
let mut printed: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
|
|
|
|
if opts.files_only {
|
|
for root in &opts.paths {
|
|
walk(root, &opts, &mut |p| {
|
|
sink.writeln(&p.display().to_string());
|
|
found = true;
|
|
!sink.capped
|
|
});
|
|
if sink.capped {
|
|
break;
|
|
}
|
|
}
|
|
return if found { 0 } else { 1 };
|
|
}
|
|
|
|
let pattern = match &opts.pattern {
|
|
Some(p) => p.clone(),
|
|
None => {
|
|
tracing::error!("rg (fallback): missing PATTERN");
|
|
return 2;
|
|
}
|
|
};
|
|
let needle = if opts.ignore_case {
|
|
pattern.to_lowercase()
|
|
} else {
|
|
pattern
|
|
};
|
|
|
|
for root in &opts.paths {
|
|
walk(root, &opts, &mut |path| {
|
|
if sink.capped {
|
|
return false;
|
|
}
|
|
if scan_file(path, &needle, &opts, &mut sink, &mut printed) {
|
|
found = true;
|
|
}
|
|
!sink.capped
|
|
});
|
|
if sink.capped {
|
|
break;
|
|
}
|
|
}
|
|
if found {
|
|
0
|
|
} else {
|
|
1
|
|
}
|
|
}
|
|
|
|
fn read_bounded_line(reader: &mut impl BufRead, max: usize) -> Option<Result<String, ()>> {
|
|
let mut buf = Vec::new();
|
|
loop {
|
|
let available = match reader.fill_buf() {
|
|
Ok([]) => {
|
|
if buf.is_empty() {
|
|
return None;
|
|
}
|
|
return match String::from_utf8(buf) {
|
|
Ok(s) => Some(Ok(s)),
|
|
Err(_) => Some(Err(())),
|
|
};
|
|
}
|
|
Ok(b) => b,
|
|
Err(_) => return None,
|
|
};
|
|
let take = available
|
|
.iter()
|
|
.position(|b| *b == b'\n')
|
|
.map_or(available.len(), |i| i + 1);
|
|
if buf.len() + take > max {
|
|
return Some(Err(()));
|
|
}
|
|
buf.extend_from_slice(&available[..take]);
|
|
reader.consume(take);
|
|
if buf.ends_with(b"\n") {
|
|
buf.pop();
|
|
return match String::from_utf8(buf) {
|
|
Ok(s) => Some(Ok(s)),
|
|
Err(_) => Some(Err(())),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
fn scan_file(
|
|
path: &Path,
|
|
needle: &str,
|
|
opts: &RgArgs,
|
|
sink: &mut CappedSink,
|
|
printed: &mut std::collections::HashSet<PathBuf>,
|
|
) -> bool {
|
|
let file = match std::fs::File::open(path) {
|
|
Ok(f) => f,
|
|
Err(_) => return false,
|
|
};
|
|
let cap = opts.context;
|
|
let mut ring: std::collections::VecDeque<String> =
|
|
std::collections::VecDeque::with_capacity(cap);
|
|
let mut tail = 0usize;
|
|
let mut last: Option<usize> = None;
|
|
let mut found = false;
|
|
|
|
let mut reader = BufReader::new(file);
|
|
let mut idx = 0usize;
|
|
loop {
|
|
let line = match read_bounded_line(&mut reader, MAX_LINE_BYTES) {
|
|
None => break,
|
|
Some(Err(())) => return found,
|
|
Some(Ok(l)) => l,
|
|
};
|
|
if sink.capped {
|
|
return found;
|
|
}
|
|
let is_match = if opts.ignore_case {
|
|
line.to_lowercase().contains(needle)
|
|
} else {
|
|
line.contains(needle)
|
|
};
|
|
|
|
if is_match {
|
|
found = true;
|
|
if opts.list_files_with_matches {
|
|
if printed.insert(path.to_path_buf()) {
|
|
sink.writeln(&path.display().to_string());
|
|
}
|
|
return found;
|
|
}
|
|
let ring_start = idx.saturating_sub(ring.len());
|
|
for (offset, prev) in ring.iter().enumerate() {
|
|
let li = ring_start + offset;
|
|
if last.is_none_or(|l| li > l) {
|
|
emit_line(path, li, prev, opts, sink);
|
|
last = Some(li);
|
|
}
|
|
}
|
|
if last.is_none_or(|l| idx > l) {
|
|
emit_line(path, idx, &line, opts, sink);
|
|
last = Some(idx);
|
|
}
|
|
tail = cap;
|
|
} else if tail > 0 {
|
|
emit_line(path, idx, &line, opts, sink);
|
|
last = Some(idx);
|
|
tail -= 1;
|
|
}
|
|
|
|
if cap > 0 {
|
|
if ring.len() == cap {
|
|
ring.pop_front();
|
|
}
|
|
ring.push_back(line);
|
|
}
|
|
idx += 1;
|
|
}
|
|
found
|
|
}
|
|
|
|
fn emit_line(path: &Path, line_idx: usize, line: &str, opts: &RgArgs, sink: &mut CappedSink) {
|
|
let prefix = if opts.line_numbers {
|
|
format!("{}:{}:", path.display(), line_idx + 1)
|
|
} else {
|
|
format!("{}:", path.display())
|
|
};
|
|
sink.writeln(&format!("{prefix}{line}"));
|
|
}
|
|
|
|
fn walk(root: &Path, opts: &RgArgs, on_file: &mut dyn FnMut(&Path) -> bool) {
|
|
if root.is_file() {
|
|
if accept(root, opts) {
|
|
on_file(root);
|
|
}
|
|
return;
|
|
}
|
|
let mut stack: Vec<(PathBuf, usize)> = vec![(root.to_path_buf(), 0)];
|
|
while let Some((dir, depth)) = stack.pop() {
|
|
let entries = match std::fs::read_dir(&dir) {
|
|
Ok(e) => e,
|
|
Err(_) => continue,
|
|
};
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let name = match path.file_name().and_then(|n| n.to_str()) {
|
|
Some(n) => n,
|
|
None => continue,
|
|
};
|
|
if name.starts_with('.') {
|
|
continue;
|
|
}
|
|
let ft = match entry.file_type() {
|
|
Ok(t) => t,
|
|
Err(_) => continue,
|
|
};
|
|
if ft.is_symlink() {
|
|
continue;
|
|
}
|
|
if ft.is_dir() {
|
|
if matches!(name, "target" | "node_modules" | "dist" | "build") {
|
|
continue;
|
|
}
|
|
if depth < MAX_WALK_DEPTH {
|
|
stack.push((path, depth + 1));
|
|
}
|
|
} else if ft.is_file() && accept(&path, opts) && !on_file(&path) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn accept(path: &Path, opts: &RgArgs) -> bool {
|
|
match &opts.glob {
|
|
None => true,
|
|
Some(g) => glob_match(g, path),
|
|
}
|
|
}
|
|
|
|
fn glob_match(pattern: &str, path: &Path) -> bool {
|
|
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
|
let full = path.to_string_lossy();
|
|
simple_glob(pattern, name) || simple_glob(pattern, &full)
|
|
}
|
|
|
|
fn simple_glob(pattern: &str, text: &str) -> bool {
|
|
let p: Vec<char> = pattern.chars().collect();
|
|
let t: Vec<char> = text.chars().collect();
|
|
|
|
let (mut pi, mut ti) = (0usize, 0usize);
|
|
let (mut star_pi, mut star_ti) = (usize::MAX, 0usize);
|
|
|
|
while ti < t.len() {
|
|
if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
|
|
pi += 1;
|
|
ti += 1;
|
|
} else if pi < p.len() && p[pi] == '*' {
|
|
star_pi = pi;
|
|
star_ti = ti;
|
|
pi += 1;
|
|
} else if star_pi != usize::MAX {
|
|
pi = star_pi + 1;
|
|
star_ti += 1;
|
|
ti = star_ti;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
while pi < p.len() && p[pi] == '*' {
|
|
pi += 1;
|
|
}
|
|
|
|
pi == p.len()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_basic_pattern_and_path() {
|
|
let opts = parse(vec!["-n".into(), "needle".into(), "src".into()]).expect("parse");
|
|
assert!(opts.line_numbers);
|
|
assert_eq!(opts.pattern.as_deref(), Some("needle"));
|
|
assert_eq!(opts.paths, vec![PathBuf::from("src")]);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_files_only() {
|
|
let opts = parse(vec!["--files".into(), ".".into()]).expect("parse");
|
|
assert!(opts.files_only);
|
|
assert_eq!(opts.paths, vec![PathBuf::from(".")]);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_rejects_unknown_flag() {
|
|
let err = parse(vec!["-Z".into(), "x".into()]).unwrap_err();
|
|
assert!(err.contains("unsupported flag"));
|
|
}
|
|
|
|
#[test]
|
|
fn capped_sink_stops_at_byte_limit() {
|
|
let mut s = CappedSink::new();
|
|
// Below cap: accepted.
|
|
s.bytes = MAX_OUTPUT_BYTES - 10;
|
|
s.writeln("12345");
|
|
assert!(!s.capped);
|
|
// This pushes us over.
|
|
s.writeln("x".repeat(20).as_str());
|
|
assert!(s.capped);
|
|
}
|
|
|
|
#[test]
|
|
fn glob_matches_simple_patterns() {
|
|
assert!(simple_glob("*.rs", "main.rs"));
|
|
assert!(simple_glob("src/*.rs", "src/main.rs"));
|
|
assert!(!simple_glob("*.rs", "main.txt"));
|
|
assert!(simple_glob("a?c", "abc"));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_finds_match_in_file() {
|
|
// End-to-end: create a file, run the fallback parser, scan it.
|
|
// We can't easily capture stdout here, so we verify scan_file
|
|
// returns true (match found).
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
let f = dir.path().join("a.txt");
|
|
std::fs::write(&f, "alpha\nNEEDLE\nbeta\n").expect("write");
|
|
let opts = parse(vec!["NEEDLE".into(), f.display().to_string()]).expect("parse");
|
|
let mut sink = CappedSink::new();
|
|
let mut printed: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
|
|
let found = scan_file(&f, "NEEDLE", &opts, &mut sink, &mut printed);
|
|
assert!(found, "expected NEEDLE to be found");
|
|
}
|
|
}
|