Skip to content

Commit 11b6449

Browse files
Merge pull request #208 from filament-dm/auto-fuzz
Two small fixes from fuzz testing.
2 parents 3021984 + a9188c7 commit 11b6449

2 files changed

Lines changed: 44 additions & 14 deletions

File tree

processors/src/docx_processor.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,22 @@ impl DocxProcessor {
1818

1919
impl FileProcessor for DocxProcessor {
2020
fn process_file(&self, path: impl AsRef<Path>) -> anyhow::Result<Document> {
21-
let docs = MarkdownDocument::from_file(path);
21+
// `docx-parser::MarkdownDocument::from_file` uses `panic!` instead of returning
22+
// `Result` when the file is missing, corrupt, or not a valid DOCX/ZIP archive.
23+
// We catch that panic here and convert it into a proper anyhow error so callers
24+
// get a clean Err(…) rather than a process-level abort.
25+
let path = path.as_ref().to_owned();
26+
let docs =
27+
std::panic::catch_unwind(move || MarkdownDocument::from_file(&path)).map_err(|e| {
28+
let msg = if let Some(s) = e.downcast_ref::<String>() {
29+
s.clone()
30+
} else if let Some(s) = e.downcast_ref::<&str>() {
31+
s.to_string()
32+
} else {
33+
"unknown panic".to_string()
34+
};
35+
anyhow::anyhow!("docx_parser panicked while opening file: {}", msg)
36+
})?;
2237
let markdown = docs.to_markdown(false);
2338
self.markdown_processor.process_document(&markdown)
2439
}

rust/src/chunkers/statistical.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,33 @@ use text_splitter::{ChunkConfig, TextSplitter};
1111
// use text_splitter::{ChunkConfig, TextSplitter};
1212
use tokenizers::Tokenizer;
1313

14-
fn median<T>(data: &[T]) -> T
14+
fn median<T>(data: &[T]) -> Option<T>
1515
where
1616
T: Copy + PartialOrd + std::ops::Add<Output = T> + std::ops::Div<Output = T> + From<u8>,
1717
{
18-
assert!(!data.is_empty(), "median requires at least one data point");
18+
if data.is_empty() {
19+
return None;
20+
}
1921
let mut sorted = data.to_vec();
20-
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
22+
// Use `unwrap_or` to handle NaN values (treat them as equal) instead of panicking.
23+
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2124
let mid = sorted.len() / 2;
22-
if sorted.len() % 2 == 0 {
25+
let result = if sorted.len() % 2 == 0 {
2326
(sorted[mid - 1] + sorted[mid]) / T::from(2u8)
2427
} else {
2528
sorted[mid]
26-
}
29+
};
30+
Some(result)
2731
}
2832

29-
fn std_dev(data: &[f32]) -> f32 {
30-
assert!(data.len() > 1, "standard deviation requires at least two data points");
33+
fn std_dev(data: &[f32]) -> Option<f32> {
34+
if data.len() < 2 {
35+
return None;
36+
}
3137
let n = data.len() as f32;
3238
let mean = data.iter().sum::<f32>() / n;
3339
let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / n;
34-
variance.sqrt()
40+
Some(variance.sqrt())
3541
}
3642

3743
pub struct StatisticalChunker {
@@ -255,7 +261,14 @@ impl StatisticalChunker {
255261
raw_similarities
256262
}
257263

258-
fn _find_optimal_threshold(&self, batch_splits: &[&str], similarities: &Vec<f32>) -> f32 {
264+
fn _find_optimal_threshold(&self, batch_splits: &[&str], similarities: &[f32]) -> f32 {
265+
// Guard: we need at least 2 similarity scores to compute median + std_dev.
266+
// With 0 scores there are no chunk boundaries to find; return a neutral threshold.
267+
// With 1 score there is no variance to measure; use that single score directly.
268+
if similarities.len() < 2 {
269+
return similarities.first().copied().unwrap_or(0.5);
270+
}
271+
259272
let tokens = self
260273
.tokenizer
261274
.encode_batch(batch_splits.to_vec(), true)
@@ -274,8 +287,10 @@ impl StatisticalChunker {
274287
.collect::<Vec<_>>();
275288

276289
// analyze the distribution of similarity scores to set initial bounds
277-
let median_score = median(similarities);
278-
let std_dev = std_dev(similarities);
290+
// Both median() and std_dev() return Option; the len() >= 2 guard above
291+
// ensures they always return Some(_) here.
292+
let median_score = median(similarities).unwrap_or(0.5);
293+
let std_dev = std_dev(similarities).unwrap_or(0.0);
279294

280295
// set initial bounds based on median and standard deviation
281296
let mut low = f32::max(0.0, median_score - std_dev);
@@ -300,7 +315,7 @@ impl StatisticalChunker {
300315
.map(|(start, end)| cumulative_token_counts[*end] - cumulative_token_counts[*start])
301316
.collect();
302317

303-
median_tokens = median(&split_token_counts);
318+
median_tokens = median(&split_token_counts).unwrap_or(0);
304319

305320
if self.min_split_tokens - self.split_token_tolerance <= median_tokens
306321
&& median_tokens <= self.max_split_tokens + self.split_token_tolerance
@@ -315,7 +330,7 @@ impl StatisticalChunker {
315330
}
316331
calculated_threshold
317332
}
318-
fn _find_split_indices(&self, similarities: &Vec<f32>, threshold: f32) -> Vec<usize> {
333+
fn _find_split_indices(&self, similarities: &[f32], threshold: f32) -> Vec<usize> {
319334
let mut split_indices = Vec::new();
320335
for (idx, score) in enumerate(similarities) {
321336
if *score < threshold {

0 commit comments

Comments
 (0)