Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions crates/bashkit/src/builtins/rg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1531,16 +1531,45 @@ fn decode_rg_content(content: &[u8], opts: &RgOptions) -> String {
}
}

const RG_GZIP_MAX_DECOMPRESSED_BYTES: usize = 10 * 1024 * 1024;
const RG_GZIP_MAX_DECOMPRESSION_RATIO: usize = 100;

fn rg_search_bytes(content: &[u8], opts: &RgOptions) -> std::result::Result<Vec<u8>, String> {
if !opts.search_zip || !is_gzip_content(content) {
return Ok(content.to_vec());
}

let mut decoder = flate2::read::GzDecoder::new(content);
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.map_err(|e| format!("gzip decompression failed: {e}"))?;
let mut chunk = [0u8; 8192];

loop {
let n = decoder
.read(&mut chunk)
.map_err(|e| format!("gzip decompression failed: {e}"))?;
if n == 0 {
break;
}

decompressed.extend_from_slice(&chunk[..n]);

if decompressed.len() > RG_GZIP_MAX_DECOMPRESSED_BYTES {
return Err(format!(
"gzip decompression exceeds {} byte limit",
RG_GZIP_MAX_DECOMPRESSED_BYTES
));
}

if !content.is_empty()
&& decompressed.len() > content.len() * RG_GZIP_MAX_DECOMPRESSION_RATIO
{
return Err(format!(
"gzip decompression ratio exceeds {}:1",
RG_GZIP_MAX_DECOMPRESSION_RATIO
));
}
}

Ok(decompressed)
}

Expand Down Expand Up @@ -3544,7 +3573,10 @@ mod tests {
FileSystem, FileSystemExt, InMemoryFs, SearchCapabilities, SearchCapable, SearchMatch,
SearchProvider, SearchQuery, SearchResults,
};
use flate2::Compression;
use flate2::write::GzEncoder;
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;

Expand Down Expand Up @@ -3783,6 +3815,37 @@ mod tests {
("/proj/compressed.txt.gz", GZIP_NEEDLE),
];

fn gzip_bytes(payload: &[u8]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(payload).expect("write gzip payload");
encoder.finish().expect("finish gzip payload")
}

fn search_zip_opts() -> RgOptions {
let args = vec!["needle".to_string()];
let mut opts = RgOptions::parse(&args).expect("parse rg options");
opts.search_zip = true;
opts
}

#[test]
fn rg_search_zip_rejects_large_gzip_decompression() {
let opts = search_zip_opts();
let payload = vec![b'a'; RG_GZIP_MAX_DECOMPRESSED_BYTES + 1];
let compressed = gzip_bytes(&payload);
let err = rg_search_bytes(&compressed, &opts).expect_err("must reject oversized gzip");
assert!(err.contains("exceeds"));
}

#[test]
fn rg_search_zip_rejects_suspicious_decompression_ratio() {
let opts = search_zip_opts();
let payload = vec![b'a'; 300_000];
let compressed = gzip_bytes(&payload);
let err = rg_search_bytes(&compressed, &opts).expect_err("must reject suspicious ratio");
assert!(err.contains("ratio exceeds"));
}

const DIFF_SYMLINK_FILES: &[(&str, &[u8])] = &[
("/targets/file.txt", b"needle\n"),
("/targets/dir/nested.txt", b"needle\n"),
Expand Down
Loading