From 393261a2bce3bad65561a599139c7446d1c2427a Mon Sep 17 00:00:00 2001 From: Federico Martinez Date: Sun, 12 Jul 2026 10:20:49 -0300 Subject: [PATCH 1/2] style: pin rustfmt style_edition 2024 and reformat the workspace Formatting previously followed rustfmt's floating default style, which changed between toolchain releases (notably import ordering under the 2024 style) and would silently break `cargo fmt --check` in CI as the pinned `stable` channel advanced. Pin it explicitly in rustfmt.toml so formatting is deterministic regardless of the installed rustfmt version, and reformat every file to that style in one pass. This pass also carries the mechanical clippy cleanups that `cargo clippy --fix` applies at the new lint level enabled in the following commit (let-else, map_or, usize::midpoint, clone_from, iterator idioms, inlined format args) plus a WriteLock `_file` -> `file` rename now that the field is read in Drop. No behavioral change; 212 tests pass, byte-identity preserved. --- rustfmt.toml | 4 + sdsearch-core/examples/append_writer.rs | 8 +- sdsearch-core/examples/bench_engine.rs | 75 ++++++++++++------- sdsearch-core/examples/diff_read.rs | 2 +- sdsearch-core/examples/optimize_bench.rs | 62 ++++++++++----- sdsearch-core/examples/perf_native.rs | 2 +- sdsearch-core/src/analysis.rs | 5 +- sdsearch-core/src/distance.rs | 2 +- sdsearch-core/src/index.rs | 4 +- sdsearch-core/src/query.rs | 12 +-- sdsearch-core/src/search.rs | 17 ++--- sdsearch-core/src/serialize.rs | 4 +- sdsearch-core/src/zsl/bytes.rs | 12 +-- sdsearch-core/src/zsl/cfs.rs | 2 +- sdsearch-core/src/zsl/norms.rs | 4 +- sdsearch-core/src/zsl/postings.rs | 2 +- sdsearch-core/src/zsl/runner.rs | 4 +- sdsearch-core/src/zsl/segment.rs | 40 +++++----- sdsearch-core/src/zsl/stored.rs | 2 +- sdsearch-core/src/zsl/terms.rs | 19 +++-- sdsearch-core/src/zsl/writer/cfs.rs | 2 +- sdsearch-core/src/zsl/writer/fnm.rs | 4 +- sdsearch-core/src/zsl/writer/index_writer.rs | 33 ++++---- sdsearch-core/src/zsl/writer/lock.rs | 6 +- sdsearch-core/src/zsl/writer/merge.rs | 7 +- sdsearch-core/src/zsl/writer/norms.rs | 4 +- sdsearch-core/src/zsl/writer/postings.rs | 2 +- sdsearch-core/src/zsl/writer/segments.rs | 9 +-- sdsearch-core/src/zsl/writer/stored.rs | 16 ++-- sdsearch-core/src/zsl/writer/terms.rs | 38 ++++------ sdsearch-core/tests/zsl_boolean_parity.rs | 4 +- sdsearch-core/tests/zsl_merge_memory_bound.rs | 41 +++++----- sdsearch-core/tests/zsl_multiseg_parity.rs | 4 +- sdsearch-core/tests/zsl_query_parity.rs | 4 +- sdsearch-core/tests/zsl_query_parity_kb.rs | 2 +- sdsearch-core/tests/zsl_unclean_shutdown.rs | 2 +- sdsearch-php/src/lib.rs | 6 +- 37 files changed, 263 insertions(+), 203 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e27957c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +# Pin the formatting style explicitly so `cargo fmt --check` is deterministic across rustfmt +# versions. Without this, the style follows rustfmt's floating default, which differs between +# toolchain releases (e.g. import ordering changed under the 2024 style) and silently drifts CI. +style_edition = "2024" diff --git a/sdsearch-core/examples/append_writer.rs b/sdsearch-core/examples/append_writer.rs index 5995650..c736f0f 100644 --- a/sdsearch-core/examples/append_writer.rs +++ b/sdsearch-core/examples/append_writer.rs @@ -9,7 +9,7 @@ //! {"name":"title","value":"...","kind":"text","stored":true}, ... ] } ] } //! kind ∈ {"text","keyword","unindexed"} (default stored=true) -use sdsearch_core::zsl::writer::{append_documents, FieldKind, WriterDoc, WriterField, WriterOpts}; +use sdsearch_core::zsl::writer::{FieldKind, WriterDoc, WriterField, WriterOpts, append_documents}; use std::path::Path; #[derive(serde::Deserialize)] @@ -77,7 +77,11 @@ fn main() { // JSON output for the harness to parse (includes timing + process peak RSS) println!( "{{\"segment\":\"{}\",\"doc_count\":{},\"generation\":{},\"elapsed_ms\":{:.3},\"peak_rss_kb\":{}}}", - report.segment_name, report.doc_count, report.generation, elapsed_ms, peak_rss_kb() + report.segment_name, + report.doc_count, + report.generation, + elapsed_ms, + peak_rss_kb() ); } diff --git a/sdsearch-core/examples/bench_engine.rs b/sdsearch-core/examples/bench_engine.rs index 53be732..7afecfd 100644 --- a/sdsearch-core/examples/bench_engine.rs +++ b/sdsearch-core/examples/bench_engine.rs @@ -55,31 +55,37 @@ struct TrackingAlloc; unsafe impl GlobalAlloc for TrackingAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let p = System.alloc(layout); - if !p.is_null() && TRACK.load(Ordering::Relaxed) { - let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); - PEAK.fetch_max(live, Ordering::Relaxed); + unsafe { + let p = System.alloc(layout); + if !p.is_null() && TRACK.load(Ordering::Relaxed) { + let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); + PEAK.fetch_max(live, Ordering::Relaxed); + } + p } - p } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - System.dealloc(ptr, layout); - if TRACK.load(Ordering::Relaxed) { - LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + unsafe { + System.dealloc(ptr, layout); + if TRACK.load(Ordering::Relaxed) { + LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + } } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - let p = System.realloc(ptr, layout, new_size); - if !p.is_null() && TRACK.load(Ordering::Relaxed) { - let old = layout.size(); - if new_size >= old { - let live = LIVE.fetch_add(new_size - old, Ordering::Relaxed) + (new_size - old); - PEAK.fetch_max(live, Ordering::Relaxed); - } else { - LIVE.fetch_sub(old - new_size, Ordering::Relaxed); + unsafe { + let p = System.realloc(ptr, layout, new_size); + if !p.is_null() && TRACK.load(Ordering::Relaxed) { + let old = layout.size(); + if new_size >= old { + let live = LIVE.fetch_add(new_size - old, Ordering::Relaxed) + (new_size - old); + PEAK.fetch_max(live, Ordering::Relaxed); + } else { + LIVE.fetch_sub(old - new_size, Ordering::Relaxed); + } } + p } - p } } @@ -87,9 +93,7 @@ unsafe impl GlobalAlloc for TrackingAlloc { /// `BENCH_TRACK_HEAP` (default: on). Startup allocations before this are minimal and precede any /// `reset_peak`, so LIVE accounting stays consistent for the whole measured region. fn init_track() -> bool { - let on = std::env::var("BENCH_TRACK_HEAP") - .map(|v| v != "0") - .unwrap_or(true); + let on = std::env::var("BENCH_TRACK_HEAP").map_or(true, |v| v != "0"); TRACK.store(on, Ordering::Relaxed); on } @@ -265,10 +269,16 @@ fn run_rebuild(n: usize, cap: usize) { let ms = t0.elapsed().as_secs_f64() * 1000.0; // total LIVE docs (base + added) via a post-commit reader — CommitReport.doc_count is the // per-session count, not the index total (see index_writer.rs commit_inner). - let doc_count = ZslIndex::open(&dir).map(|r| r.num_docs()).unwrap_or(0); + let doc_count = ZslIndex::open(&dir).map_or(0, |r| r.num_docs()); println!( "{{\"engine\":\"native\",\"pass\":\"{}\",\"workload\":\"rebuild\",\"n\":{},\"cap\":{},\"ms\":{:.3},\"heap_peak_kb\":{},\"rss_peak_kb\":{},\"doc_count\":{}}}", - pass_label(), n, cap, ms, heap_peak_kb(), rss_peak_kb(), doc_count + pass_label(), + n, + cap, + ms, + heap_peak_kb(), + rss_peak_kb(), + doc_count ); std::fs::remove_dir_all(&dir).ok(); } @@ -296,10 +306,18 @@ fn run_churn(n: usize, cap: usize) { } w.commit().expect("commit failed"); let ms = t0.elapsed().as_secs_f64() * 1000.0; - let doc_count = ZslIndex::open(&dir).map(|r| r.num_docs()).unwrap_or(0); + let doc_count = ZslIndex::open(&dir).map_or(0, |r| r.num_docs()); println!( "{{\"engine\":\"native\",\"pass\":\"{}\",\"workload\":\"churn\",\"n\":{},\"cap\":{},\"pct1\":{},\"ms\":{:.3},\"heap_peak_kb\":{},\"rss_peak_kb\":{},\"doc_count_before\":{},\"doc_count\":{}}}", - pass_label(), n, cap, one_pct, ms, heap_peak_kb(), rss_peak_kb(), doc_count_before, doc_count + pass_label(), + n, + cap, + one_pct, + ms, + heap_peak_kb(), + rss_peak_kb(), + doc_count_before, + doc_count ); std::fs::remove_dir_all(&dir).ok(); } @@ -354,7 +372,12 @@ fn run_search(n: usize, iters: usize) { } println!( "{{\"engine\":\"native\",\"pass\":\"{}\",\"workload\":\"search\",\"n\":{},\"iters\":{},\"heap_peak_kb\":{},\"rss_peak_kb\":{},{}}}", - pass_label(), n, iters, heap_peak_kb(), rss_peak_kb(), parts.join(",") + pass_label(), + n, + iters, + heap_peak_kb(), + rss_peak_kb(), + parts.join(",") ); std::fs::remove_dir_all(&dir).ok(); } @@ -362,7 +385,7 @@ fn run_search(n: usize, iters: usize) { fn main() { init_track(); // TIME pass (BENCH_TRACK_HEAP=0) vs HEAP pass (default), before any measured op. let args: Vec = std::env::args().collect(); - let workload = args.get(1).map(String::as_str).unwrap_or("rebuild"); + let workload = args.get(1).map_or("rebuild", String::as_str); let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(1000); match workload { "rebuild" => run_rebuild(n, args.get(3).and_then(|s| s.parse().ok()).unwrap_or(1000)), diff --git a/sdsearch-core/examples/diff_read.rs b/sdsearch-core/examples/diff_read.rs index d209a79..ff746c8 100644 --- a/sdsearch-core/examples/diff_read.rs +++ b/sdsearch-core/examples/diff_read.rs @@ -6,7 +6,7 @@ //! queries.json: {"fields":["body","tag"],"queries":[{"name":"work","text":"work"},...]} use sdsearch_core::index::IndexReader; -use sdsearch_core::query::{build_query, search, QueryParams}; +use sdsearch_core::query::{QueryParams, build_query, search}; use sdsearch_core::zsl::index::ZslIndex; use std::collections::BTreeMap; use std::path::Path; diff --git a/sdsearch-core/examples/optimize_bench.rs b/sdsearch-core/examples/optimize_bench.rs index 1c1c378..6b380ed 100644 --- a/sdsearch-core/examples/optimize_bench.rs +++ b/sdsearch-core/examples/optimize_bench.rs @@ -36,29 +36,35 @@ struct TrackingAlloc; unsafe impl GlobalAlloc for TrackingAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let p = System.alloc(layout); - if !p.is_null() { - let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); - PEAK.fetch_max(live, Ordering::Relaxed); + unsafe { + let p = System.alloc(layout); + if !p.is_null() { + let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); + PEAK.fetch_max(live, Ordering::Relaxed); + } + p } - p } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - System.dealloc(ptr, layout); - LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + unsafe { + System.dealloc(ptr, layout); + LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - let p = System.realloc(ptr, layout, new_size); - if !p.is_null() { - let old = layout.size(); - if new_size >= old { - let live = LIVE.fetch_add(new_size - old, Ordering::Relaxed) + (new_size - old); - PEAK.fetch_max(live, Ordering::Relaxed); - } else { - LIVE.fetch_sub(old - new_size, Ordering::Relaxed); + unsafe { + let p = System.realloc(ptr, layout, new_size); + if !p.is_null() { + let old = layout.size(); + if new_size >= old { + let live = LIVE.fetch_add(new_size - old, Ordering::Relaxed) + (new_size - old); + PEAK.fetch_max(live, Ordering::Relaxed); + } else { + LIVE.fetch_sub(old - new_size, Ordering::Relaxed); + } } + p } - p } } @@ -265,7 +271,13 @@ fn run_existing(args: &[String]) { println!( "{{\"source\":\"{}\",\"n_extra\":{},\"segments_before\":{},\"doc_count\":{},\"optimize_ms\":{:.2},\"heap_peak_kb\":{},\"vmhwm_kb\":{}}}", - src.display(), n_extra, segments_before, report.doc_count, optimize_ms, heap_peak_kb, vmhwm_kb() + src.display(), + n_extra, + segments_before, + report.doc_count, + optimize_ms, + heap_peak_kb, + vmhwm_kb() ); std::fs::remove_dir_all(scratch).ok(); @@ -306,7 +318,13 @@ fn run_hot(args: &[String]) { println!( "{{\"mode\":\"hot\",\"n\":{},\"cap\":{},\"segments_before\":{},\"doc_count\":{},\"optimize_ms\":{:.2},\"heap_peak_kb\":{},\"vmhwm_kb\":{}}}", - n, cap, segments_before, report.doc_count, optimize_ms, heap_peak_kb, vmhwm_kb() + n, + cap, + segments_before, + report.doc_count, + optimize_ms, + heap_peak_kb, + vmhwm_kb() ); std::fs::remove_dir_all(&dir).ok(); @@ -354,7 +372,13 @@ fn main() { println!( "{{\"n\":{},\"cap\":{},\"segments_before\":{},\"doc_count\":{},\"optimize_ms\":{:.2},\"heap_peak_kb\":{},\"vmhwm_kb\":{}}}", - n, cap, segments_before, report.doc_count, optimize_ms, heap_peak_kb, vmhwm_kb() + n, + cap, + segments_before, + report.doc_count, + optimize_ms, + heap_peak_kb, + vmhwm_kb() ); std::fs::remove_dir_all(&dir).ok(); diff --git a/sdsearch-core/examples/perf_native.rs b/sdsearch-core/examples/perf_native.rs index 2845277..24b064f 100644 --- a/sdsearch-core/examples/perf_native.rs +++ b/sdsearch-core/examples/perf_native.rs @@ -1,7 +1,7 @@ //! perf of the native reader over a large user-provided ZSL index. //! Usage: SDSEARCH_PERF_INDEX=/path/to/index cargo run -p sdsearch-core --release --example perf_native //! If the env var is unset, exits 0 without running (for CI/dev without the index). -use sdsearch_core::query::{build_query, search, QueryParams}; +use sdsearch_core::query::{QueryParams, build_query, search}; use sdsearch_core::zsl::index::ZslIndex; use std::time::Instant; diff --git a/sdsearch-core/src/analysis.rs b/sdsearch-core/src/analysis.rs index 5e58bb3..4332c2b 100644 --- a/sdsearch-core/src/analysis.rs +++ b/sdsearch-core/src/analysis.rs @@ -2,11 +2,10 @@ //! tokenization regex deviates from stock Zend Lucene: tokens keep - _ . : # / @ //! (emails, URLs, and ticket refs stay as a single term). -use once_cell::sync::Lazy; use regex::Regex; -static TOKEN_RE: Lazy = - Lazy::new(|| Regex::new(r"[\p{L}\p{N}\-_.:#/@]+[\p{L}\p{N}]?").unwrap()); +static TOKEN_RE: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"[\p{L}\p{N}\-_.:#/@]+[\p{L}\p{N}]?").unwrap()); /// tokenizes and lowercases, replicating the legacy analyzer pub fn analyze(text: &str) -> Vec { diff --git a/sdsearch-core/src/distance.rs b/sdsearch-core/src/distance.rs index 437fe0f..d1269b3 100644 --- a/sdsearch-core/src/distance.rs +++ b/sdsearch-core/src/distance.rs @@ -16,7 +16,7 @@ pub fn levenshtein_bytes(a: &[u8], b: &[u8]) -> usize { for i in 1..=n { cur[0] = i; for j in 1..=m { - let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 }; + let cost = usize::from(a[i - 1] != b[j - 1]); cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost); } std::mem::swap(&mut prev, &mut cur); diff --git a/sdsearch-core/src/index.rs b/sdsearch-core/src/index.rs index ee1ec19..f442570 100644 --- a/sdsearch-core/src/index.rs +++ b/sdsearch-core/src/index.rs @@ -148,7 +148,7 @@ impl MemoryIndex { let mut sorted = positions.clone(); sorted.sort_unstable(); for p in sorted { - write_vint(&mut postings_bin, p as u64); + write_vint(&mut postings_bin, u64::from(p)); } prev = doc_id; } @@ -203,7 +203,7 @@ impl IndexReader for MemoryIndex { fn doc_freq(&self, field: &str, term: &str) -> usize { self.postings .get(&(field.to_string(), term.to_string())) - .map_or(0, |p| p.len()) + .map_or(0, std::collections::HashMap::len) } /// postings of (field, term): iterator of (doc_id, term_freq); freq = number of positions diff --git a/sdsearch-core/src/query.rs b/sdsearch-core/src/query.rs index a5d34b9..51d6be0 100644 --- a/sdsearch-core/src/query.rs +++ b/sdsearch-core/src/query.rs @@ -3,7 +3,7 @@ use crate::index::IndexReader; use crate::search::{ - finalize, fuzzy_terms, phrase_scores, term_scores, union_scores, wildcard_terms, Hit, + Hit, finalize, fuzzy_terms, phrase_scores, term_scores, union_scores, wildcard_terms, }; use std::collections::HashMap; @@ -69,7 +69,7 @@ fn eval(index: &impl IndexReader, q: &Query) -> HashMap { let mut acc: HashMap = HashMap::new(); for f in target_fields(index, field) { let terms = wildcard_terms(index, &f, pattern, *min_prefix_len); - let refs: Vec<&str> = terms.iter().map(|s| s.as_str()).collect(); + let refs: Vec<&str> = terms.iter().map(std::string::String::as_str).collect(); for (id, s) in union_scores(index, &f, &refs) { *acc.entry(id).or_insert(0.0) += s; } @@ -85,7 +85,7 @@ fn eval(index: &impl IndexReader, q: &Query) -> HashMap { let mut acc: HashMap = HashMap::new(); for f in target_fields(index, field) { let terms = fuzzy_terms(index, &f, text, *similarity, *prefix_len); - let refs: Vec<&str> = terms.iter().map(|s| s.as_str()).collect(); + let refs: Vec<&str> = terms.iter().map(std::string::String::as_str).collect(); for (id, s) in union_scores(index, &f, &refs) { *acc.entry(id).or_insert(0.0) += s; } @@ -93,7 +93,7 @@ fn eval(index: &impl IndexReader, q: &Query) -> HashMap { acc } Query::Phrase { field, terms } => { - let refs: Vec<&str> = terms.iter().map(|s| s.as_str()).collect(); + let refs: Vec<&str> = terms.iter().map(std::string::String::as_str).collect(); phrase_scores(index, field, &refs) } Query::Boolean { clauses } => eval_boolean(index, clauses), @@ -398,7 +398,7 @@ mod tests { } fn ids(hits: &[crate::search::Hit]) -> Vec { let mut v: Vec = hits.iter().map(|h| h.id).collect(); - v.sort(); + v.sort_unstable(); v } @@ -562,7 +562,7 @@ mod tests { fn build_query_empty_field_is_error() { let mut p = params("vpn"); p.where_groups = vec![WhereGroup { - field: "".into(), + field: String::new(), values: vec!["x".into()], occur: Occur::Should, }]; diff --git a/sdsearch-core/src/search.rs b/sdsearch-core/src/search.rs index 59f265e..5a25ff0 100644 --- a/sdsearch-core/src/search.rs +++ b/sdsearch-core/src/search.rs @@ -79,9 +79,8 @@ pub(crate) fn wildcard_terms( // preg_quote + wildcard replacement (equivalent to ZSL) let escaped = regex::escape(pattern); let regex_str = format!("^{}$", escaped.replace("\\*", ".*").replace("\\?", ".")); - let re = match regex::Regex::new(®ex_str) { - Ok(r) => r, - Err(_) => return Vec::new(), + let Ok(re) = regex::Regex::new(®ex_str) else { + return Vec::new(); }; index .terms_with_prefix(field, prefix) @@ -101,7 +100,7 @@ pub(crate) fn fuzzy_terms( min_similarity: f32, prefix_length: usize, ) -> Vec { - let min_sim = min_similarity as f64; + let min_sim = f64::from(min_similarity); // exact prefix = first prefix_length UTF-8 chars let prefix: String = term.chars().take(prefix_length).collect(); let prefix_byte_len = prefix.len(); @@ -194,7 +193,7 @@ pub(crate) fn phrase_scores( if is_match { let s: f32 = (0..terms.len()) .map(|i| { - let tf = per_term[i].0.get(&doc).map(|v| v.len() as u32).unwrap_or(0); + let tf = per_term[i].0.get(&doc).map_or(0, |v| v.len() as u32); score_with_idf(per_term[i].1, tf, index.field_len(doc, field)) }) .sum(); @@ -264,7 +263,7 @@ pub fn wildcard_query( limit: usize, ) -> Vec { let terms = wildcard_terms(index, field, pattern, min_prefix_len); - let refs: Vec<&str> = terms.iter().map(|s| s.as_str()).collect(); + let refs: Vec<&str> = terms.iter().map(std::string::String::as_str).collect(); finalize(index, union_scores(index, field, &refs), min_score, limit) } @@ -279,7 +278,7 @@ pub fn fuzzy_query( limit: usize, ) -> Vec { let terms = fuzzy_terms(index, field, term, min_similarity, prefix_length); - let refs: Vec<&str> = terms.iter().map(|s| s.as_str()).collect(); + let refs: Vec<&str> = terms.iter().map(std::string::String::as_str).collect(); finalize(index, union_scores(index, field, &refs), min_score, limit) } @@ -376,7 +375,7 @@ mod tests { // "test*" => terms testing, tested => docs 0 and 1 let hits = wildcard_query(&wildcard_corpus(), "body", "test*", 2, 0.0, 10); let mut ids: Vec = hits.iter().map(|h| h.id).collect(); - ids.sort(); + ids.sort_unstable(); assert_eq!(ids, vec![0, 1]); } @@ -446,7 +445,7 @@ mod tests { // "brown fox": doc0 (brown@1,fox@2) and doc1 (brown@0,fox@1) let hits = phrase_query(&phrase_corpus(), "body", &["brown", "fox"], 0.0, 10); let mut ids: Vec = hits.iter().map(|h| h.id).collect(); - ids.sort(); + ids.sort_unstable(); assert_eq!(ids, vec![0, 1]); } diff --git a/sdsearch-core/src/serialize.rs b/sdsearch-core/src/serialize.rs index 4270b68..7da36b2 100644 --- a/sdsearch-core/src/serialize.rs +++ b/sdsearch-core/src/serialize.rs @@ -37,7 +37,7 @@ pub fn read_vint(data: &[u8], pos: &mut usize) -> std::io::Result { "overlong varint (more than 64 bits)", )); } - result |= ((byte & 0x7f) as u64) << shift; + result |= u64::from(byte & 0x7f) << shift; if byte & 0x80 == 0 { break; } @@ -61,7 +61,7 @@ mod tests { 16_383, 16_384, 1_000_000, - u32::MAX as u64, + u64::from(u32::MAX), ] { let mut buf = Vec::new(); write_vint(&mut buf, v); diff --git a/sdsearch-core/src/zsl/bytes.rs b/sdsearch-core/src/zsl/bytes.rs index fc67622..6a5777d 100644 --- a/sdsearch-core/src/zsl/bytes.rs +++ b/sdsearch-core/src/zsl/bytes.rs @@ -93,14 +93,14 @@ pub fn read_modified_utf8(data: &[u8], pos: &mut usize) -> io::Result { s.push(b0 as char); } else if b0 & 0xE0 == 0xC0 { let b1 = read_byte(data, pos)?; - let cp = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32); + let cp = (u32::from(b0 & 0x1F) << 6) | u32::from(b1 & 0x3F); s.push(char::from_u32(cp).unwrap_or('\u{FFFD}')); // C0 80 -> 0 } else if b0 & 0xF0 == 0xE0 { // 3-byte (BMP). let b1 = read_byte(data, pos)?; let b2 = read_byte(data, pos)?; let cp = - (((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32); + (u32::from(b0 & 0x0F) << 12) | (u32::from(b1 & 0x3F) << 6) | u32::from(b2 & 0x3F); s.push(char::from_u32(cp).unwrap_or('\u{FFFD}')); } else { // 4-byte (supplementary plane). ZSL's PHP writer stores standard UTF-8 (not Java's @@ -110,10 +110,10 @@ pub fn read_modified_utf8(data: &[u8], pos: &mut usize) -> io::Result { let b1 = read_byte(data, pos)?; let b2 = read_byte(data, pos)?; let b3 = read_byte(data, pos)?; - let cp = (((b0 & 0x07) as u32) << 18) - | (((b1 & 0x3F) as u32) << 12) - | (((b2 & 0x3F) as u32) << 6) - | ((b3 & 0x3F) as u32); + let cp = (u32::from(b0 & 0x07) << 18) + | (u32::from(b1 & 0x3F) << 12) + | (u32::from(b2 & 0x3F) << 6) + | u32::from(b3 & 0x3F); s.push(char::from_u32(cp).unwrap_or('\u{FFFD}')); } } diff --git a/sdsearch-core/src/zsl/cfs.rs b/sdsearch-core/src/zsl/cfs.rs index 5ba3c89..0002a4e 100644 --- a/sdsearch-core/src/zsl/cfs.rs +++ b/sdsearch-core/src/zsl/cfs.rs @@ -71,7 +71,7 @@ mod tests { std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .expect("no .cfs in fixture — regenerate with sdsearch_dump_zsl_index.php") } diff --git a/sdsearch-core/src/zsl/norms.rs b/sdsearch-core/src/zsl/norms.rs index 3b68735..ae52881 100644 --- a/sdsearch-core/src/zsl/norms.rs +++ b/sdsearch-core/src/zsl/norms.rs @@ -25,8 +25,8 @@ pub fn decode_norm(b: u8) -> f32 { if b == 0 { return 0.0; } - let mantissa = (b & 0x07) as u32; - let exponent = ((b >> 3) & 0x1F) as u32; + let mantissa = u32::from(b & 0x07); + let exponent = u32::from((b >> 3) & 0x1F); let bits = (exponent << 24) | (mantissa << 21); f32::from_bits(bits) } diff --git a/sdsearch-core/src/zsl/postings.rs b/sdsearch-core/src/zsl/postings.rs index e14484a..d24577f 100644 --- a/sdsearch-core/src/zsl/postings.rs +++ b/sdsearch-core/src/zsl/postings.rs @@ -129,7 +129,7 @@ mod tests { let path = std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .unwrap(); CompoundFile::open(&path).unwrap() } diff --git a/sdsearch-core/src/zsl/runner.rs b/sdsearch-core/src/zsl/runner.rs index a281dd5..71732db 100644 --- a/sdsearch-core/src/zsl/runner.rs +++ b/sdsearch-core/src/zsl/runner.rs @@ -3,7 +3,7 @@ //! limit==0 = unlimited). use crate::analysis::analyze; -use crate::query::{build_query, search, Occur, Query, QueryParams}; +use crate::query::{Occur, Query, QueryParams, build_query, search}; use crate::search::Hit; use crate::zsl::index::ZslIndex; use std::collections::HashSet; @@ -77,7 +77,7 @@ mod tests { } fn ids(hits: &[Hit]) -> Vec { let mut v: Vec = hits.iter().map(|h| h.id).collect(); - v.sort(); + v.sort_unstable(); v } diff --git a/sdsearch-core/src/zsl/segment.rs b/sdsearch-core/src/zsl/segment.rs index da11ec0..a40e3b2 100644 --- a/sdsearch-core/src/zsl/segment.rs +++ b/sdsearch-core/src/zsl/segment.rs @@ -2,10 +2,10 @@ use crate::index::IndexReader; use crate::zsl::cfs::CompoundFile; use crate::zsl::deletes::DeletedDocs; -use crate::zsl::fields::{read_field_infos, FieldInfo}; +use crate::zsl::fields::{FieldInfo, read_field_infos}; use crate::zsl::norms::{approx_field_len, read_norms}; use crate::zsl::postings::{for_each_posting, read_all_positions, read_freqs, read_positions}; -use crate::zsl::stored::{read_stored_fields, read_stored_raw, StoredRaw}; +use crate::zsl::stored::{StoredRaw, read_stored_fields, read_stored_raw}; use crate::zsl::terms::{TermCursor, TermDict, TermInfo}; use std::collections::HashMap; use std::path::Path; @@ -50,7 +50,7 @@ impl ZslSegment { /// column of the field's raw norm bytes (one per doc, incl. deletes), or `None`. /// The merge COPIES them verbatim (no re-encoding) into the merged segment. pub fn norm_bytes(&self, field: &str) -> Option<&[u8]> { - self.norms.get(field).map(|v| v.as_slice()) + self.norms.get(field).map(std::vec::Vec::as_slice) } /// all `(field, text)` terms of the segment (to walk them during the merge). @@ -126,11 +126,11 @@ impl ZslSegment { pub fn open(index_dir: &Path) -> std::io::Result { let cfs_path = std::fs::read_dir(index_dir)? .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .ok_or_else(|| std::io::Error::other("no .cfs in index dir"))?; let del_path = std::fs::read_dir(index_dir)? .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "del").unwrap_or(false)); + .find(|p| p.extension().is_some_and(|x| x == "del")); Self::open_from(index_dir, &cfs_path, del_path) } @@ -231,8 +231,7 @@ impl IndexReader for ZslSegment { fn doc_freq(&self, field: &str, term: &str) -> usize { self.dict .info(field, term) - .map(|ti| ti.doc_freq as usize) - .unwrap_or(0) + .map_or(0, |ti| ti.doc_freq as usize) } fn postings_for(&self, field: &str, term: &str) -> Vec<(usize, u32)> { @@ -251,8 +250,7 @@ impl IndexReader for ZslSegment { self.norms .get(field) .and_then(|v| v.get(doc_id)) - .map(|b| approx_field_len(*b)) - .unwrap_or(1) + .map_or(1, |b| approx_field_len(*b)) } fn stored_fields(&self, doc_id: usize) -> HashMap { @@ -494,29 +492,31 @@ mod tests { // must also agree with positions_all's doc set (both are delete-filtered). let mut expected: Vec = s.positions_all(field, term).into_keys().collect(); - expected.sort(); + expected.sort_unstable(); let mut got = live.clone(); - got.sort(); + got.sort_unstable(); assert_eq!(got, expected); } #[test] fn merge_accessors_expose_fields_deletes_norms_terms_stored() { let s = seg(); // incidents fixture, 4 docs, no deletes - // field_infos: includes title (indexed) - assert!(s - .field_infos() - .iter() - .any(|f| f.name == "title" && f.is_indexed)); + // field_infos: includes title (indexed) + assert!( + s.field_infos() + .iter() + .any(|f| f.name == "title" && f.is_indexed) + ); // is_deleted: nothing deleted in the fixture assert!(!s.is_deleted(0)); // norm_bytes: title has a 4-byte column (one per doc) - assert_eq!(s.norm_bytes("title").map(|c| c.len()), Some(4)); + assert_eq!(s.norm_bytes("title").map(<[u8]>::len), Some(4)); assert!(s.norm_bytes("campo_inexistente").is_none()); // all_terms: title:new present - assert!(s - .all_terms() - .contains(&("title".to_string(), "new".to_string()))); + assert!( + s.all_terms() + .contains(&("title".to_string(), "new".to_string())) + ); // stored_raw doc 0: contains id_key="165" (same value as stored_fields) let raw = s.stored_raw(0).unwrap(); let names = s.field_infos(); diff --git a/sdsearch-core/src/zsl/stored.rs b/sdsearch-core/src/zsl/stored.rs index 158d56d..7ce7a53 100644 --- a/sdsearch-core/src/zsl/stored.rs +++ b/sdsearch-core/src/zsl/stored.rs @@ -92,7 +92,7 @@ mod tests { let path = std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .unwrap(); CompoundFile::open(&path).unwrap() } diff --git a/sdsearch-core/src/zsl/terms.rs b/sdsearch-core/src/zsl/terms.rs index 7e27e5d..bb2c0fe 100644 --- a/sdsearch-core/src/zsl/terms.rs +++ b/sdsearch-core/src/zsl/terms.rs @@ -117,7 +117,7 @@ impl EagerTermDict { let target = term.as_bytes(); let (mut lo, mut hi) = (0usize, ft.len()); while lo < hi { - let mid = (lo + hi) / 2; + let mid = usize::midpoint(lo, hi); match ft.term(mid).cmp(target) { std::cmp::Ordering::Less => lo = mid + 1, std::cmp::Ordering::Greater => hi = mid, @@ -130,15 +130,14 @@ impl EagerTermDict { /// Terms of `field` that start with `prefix`. Locates the range start by /// binary search and walks until a term stops matching the prefix. pub fn terms_with_prefix(&self, field: &str, prefix: &str) -> Vec { - let ft = match self.by_field.get(field) { - Some(f) => f, - None => return Vec::new(), + let Some(ft) = self.by_field.get(field) else { + return Vec::new(); }; let pb = prefix.as_bytes(); // partition_point: first index with term(i) >= prefix. let (mut lo, mut hi) = (0usize, ft.len()); while lo < hi { - let mid = (lo + hi) / 2; + let mid = usize::midpoint(lo, hi); if ft.term(mid) < pb { lo = mid + 1; } else { @@ -247,7 +246,7 @@ impl TermDict { let _pfx = read_vint(tii, &mut pos)?; let _suf = read_modified_utf8(tii, &mut pos)?; let _raw_field = read_i32_be(tii, &mut pos)?; // raw Int, not VInt - // consume the literal 0x0F marker byte: + // consume the literal 0x0F marker byte: if pos >= tii.len() { return Err(std::io::Error::other("tii: truncated synthetic entry")); } @@ -428,7 +427,7 @@ impl TermDict { &self.field_names, ) { Ok((f, t, ti)) => { - prev = t.clone(); + prev.clone_from(&t); out.push((f, t, ti)); } Err(_) => break, @@ -599,7 +598,7 @@ mod tests { let path = std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .unwrap(); let cf = CompoundFile::open(&path).unwrap(); let fnm = cf @@ -675,7 +674,7 @@ mod tests { let path = std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .unwrap(); let cf = CompoundFile::open(&path).unwrap(); let find = |ext: &str| cf.names().into_iter().find(|n| n.ends_with(ext)).unwrap(); @@ -701,7 +700,7 @@ mod tests { let path = std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .unwrap(); let cf = CompoundFile::open(&path).unwrap(); let find = |ext: &str| cf.names().into_iter().find(|n| n.ends_with(ext)).unwrap(); diff --git a/sdsearch-core/src/zsl/writer/cfs.rs b/sdsearch-core/src/zsl/writer/cfs.rs index a2e4a12..9050c1c 100644 --- a/sdsearch-core/src/zsl/writer/cfs.rs +++ b/sdsearch-core/src/zsl/writer/cfs.rs @@ -40,7 +40,7 @@ pub enum CfsSource<'a> { Path(&'a Path), } -impl<'a> CfsSource<'a> { +impl CfsSource<'_> { fn len(&self) -> io::Result { match self { CfsSource::Mem(data) => Ok(data.len() as u64), diff --git a/sdsearch-core/src/zsl/writer/fnm.rs b/sdsearch-core/src/zsl/writer/fnm.rs index 4811e92..ecc8039 100644 --- a/sdsearch-core/src/zsl/writer/fnm.rs +++ b/sdsearch-core/src/zsl/writer/fnm.rs @@ -10,7 +10,7 @@ pub fn write_fnm(fields: &[FieldMeta]) -> Vec { write_vint(&mut out, fields.len() as u64); for f in fields { write_modified_utf8(&mut out, &f.name); - out.push(if f.indexed { 0x01 } else { 0x00 }); + out.push(u8::from(f.indexed)); } out } @@ -18,7 +18,7 @@ pub fn write_fnm(fields: &[FieldMeta]) -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::zsl::fields::{read_field_infos, FieldInfo}; + use crate::zsl::fields::{FieldInfo, read_field_infos}; #[test] fn fnm_roundtrips_names_and_indexed_flag_through_reader() { diff --git a/sdsearch-core/src/zsl/writer/index_writer.rs b/sdsearch-core/src/zsl/writer/index_writer.rs index 4843812..2b24dc6 100644 --- a/sdsearch-core/src/zsl/writer/index_writer.rs +++ b/sdsearch-core/src/zsl/writer/index_writer.rs @@ -8,7 +8,7 @@ use super::lock::WriteLock; use super::merge; use super::segments::{self, Generation, NewSegment}; -use super::{write_segment_cfs, WriterDoc, WriterOpts}; +use super::{WriterDoc, WriterOpts, write_segment_cfs}; use crate::index::IndexReader; use crate::zsl::deletes::DeletedDocs; use crate::zsl::index::ZslIndex; @@ -106,7 +106,11 @@ impl IndexWriter { /// (over-counting deletes). The host application never re-deletes an already-deleted doc, so this never manifests. pub fn document_count(&self) -> usize { let flushed: usize = self.flushed.iter().map(|s| s.doc_count as usize).sum(); - let pending: usize = self.pending_deletes.values().map(|s| s.len()).sum(); + let pending: usize = self + .pending_deletes + .values() + .map(std::collections::BTreeSet::len) + .sum(); (self.base_live_docs + flushed + self.buffer.len()).saturating_sub(pending) } @@ -518,7 +522,7 @@ mod tests { assert!(!dir.join("_3.cfs").exists()); // orphan cleaned by Drop assert!(!dir.join("segments_7").exists()); // generation NOT flipped assert_eq!(ZslIndex::open(&dir).unwrap().num_docs(), 20); // intact - // lock released → re-open OK + // lock released → re-open OK let _w = IndexWriter::open(&dir, WriterOpts::default()).unwrap(); std::fs::remove_dir_all(&dir).ok(); } @@ -754,12 +758,14 @@ mod tests { assert!(!dir.join("segments_9").exists()); // current (11 == "b") and immediately-previous (10 == "a") survive: grace window for // lock-free concurrent readers that read segments.gen just before the last flip. - assert!(dir - .join(format!("segments_{}", crate::zsl::segments::to_base36(10))) - .exists()); - assert!(dir - .join(format!("segments_{}", crate::zsl::segments::to_base36(11))) - .exists()); + assert!( + dir.join(format!("segments_{}", crate::zsl::segments::to_base36(10))) + .exists() + ); + assert!( + dir.join(format!("segments_{}", crate::zsl::segments::to_base36(11))) + .exists() + ); // segments.gen and the actual segment data (.cfs) are untouched by the pruning. assert!(dir.join("segments.gen").exists()); @@ -772,7 +778,7 @@ mod tests { #[test] fn commit_prunes_using_numeric_not_lexical_order_across_base36_rollover() { let dir = temp_kb_full(); // KB: gen 6 - // enough commits to cross the base36 rollover ("z" = 35 -> "10" = 36), plus margin. + // enough commits to cross the base36 rollover ("z" = 35 -> "10" = 36), plus margin. for i in 0..35 { let mut w = IndexWriter::open(&dir, WriterOpts::default()).unwrap(); w.add_document(doc_mark(100 + i)).unwrap(); @@ -821,9 +827,10 @@ mod tests { assert!(!dir.join("segments_8").exists()); // current (10 == "a") and immediately-previous (9) survive. assert!(dir.join("segments_9").exists()); - assert!(dir - .join(format!("segments_{}", crate::zsl::segments::to_base36(10))) - .exists()); + assert!( + dir.join(format!("segments_{}", crate::zsl::segments::to_base36(10))) + .exists() + ); assert!(dir.join("segments.gen").exists()); let idx = ZslIndex::open(&dir).unwrap(); diff --git a/sdsearch-core/src/zsl/writer/lock.rs b/sdsearch-core/src/zsl/writer/lock.rs index 998761b..79fa05f 100644 --- a/sdsearch-core/src/zsl/writer/lock.rs +++ b/sdsearch-core/src/zsl/writer/lock.rs @@ -10,7 +10,7 @@ use std::path::Path; #[derive(Debug)] pub struct WriteLock { - _file: File, // keeping the File alive == holding the lock (released when dropped) + file: File, // keeping the File alive == holding the lock (released when dropped) } impl WriteLock { @@ -25,7 +25,7 @@ impl WriteLock { .truncate(false) .open(&path)?; match file.try_lock() { - Ok(()) => Ok(WriteLock { _file: file }), + Ok(()) => Ok(WriteLock { file }), Err(TryLockError::WouldBlock) => Err(std::io::Error::new( std::io::ErrorKind::WouldBlock, "write lock already held", @@ -38,7 +38,7 @@ impl WriteLock { impl Drop for WriteLock { fn drop(&mut self) { // best-effort: release the lock explicitly (it is released anyway when the File closes). - let _ = self._file.unlock(); + let _ = self.file.unlock(); } } diff --git a/sdsearch-core/src/zsl/writer/merge.rs b/sdsearch-core/src/zsl/writer/merge.rs index 767b999..e06dd2d 100644 --- a/sdsearch-core/src/zsl/writer/merge.rs +++ b/sdsearch-core/src/zsl/writer/merge.rs @@ -11,7 +11,7 @@ //! sorts by new doc-id; drops terms with no live docs. //! 4. serializes reusing the `write_segment_cfs` primitives (norms COPIED verbatim via write_norms_raw). -use super::cfs::{write_cfs_streaming, CfsSource}; +use super::cfs::{CfsSource, write_cfs_streaming}; use super::invert::{FieldMeta, StoredField, TermPostings}; use super::{assemble_cfs, fnm, norms, stored, terms}; use crate::index::IndexReader; @@ -368,7 +368,10 @@ fn merge_streaming_inner( // max-heap, so keys are wrapped in Reverse) drives the k-way merge. Keys are cloned into the // heap (bounded: at most one small (field, term) per segment at a time) because the cursors' // borrows end at each `peek`, so we cannot hold `&str` across an `advance`. - let mut cursors: Vec<_> = segs.iter().map(|s| s.term_cursor()).collect(); + let mut cursors: Vec<_> = segs + .iter() + .map(super::super::segment::ZslSegment::term_cursor) + .collect(); let mut heap: BinaryHeap> = BinaryHeap::new(); for (si, cur) in cursors.iter().enumerate() { if let Some((f, t)) = cur.peek() { diff --git a/sdsearch-core/src/zsl/writer/norms.rs b/sdsearch-core/src/zsl/writer/norms.rs index c84ed7e..e0d0105 100644 --- a/sdsearch-core/src/zsl/writer/norms.rs +++ b/sdsearch-core/src/zsl/writer/norms.rs @@ -8,10 +8,8 @@ //! `byte b>0 -> f32::from_bits((b<<21) + 0x30000000)`, and `_floatToByte` (binary search + //! round to nearest) identical to `Zend_Search_Lucene_Search_Similarity`. -use once_cell::sync::Lazy; - /// SmallFloat table: `NORM_TABLE[b]` = the float value that byte `b` decodes to. -static NORM_TABLE: Lazy<[f32; 256]> = Lazy::new(|| { +static NORM_TABLE: std::sync::LazyLock<[f32; 256]> = std::sync::LazyLock::new(|| { let mut t = [0f32; 256]; for (b, slot) in t.iter_mut().enumerate().skip(1) { *slot = f32::from_bits(((b as u32) << 21).wrapping_add(0x3000_0000)); diff --git a/sdsearch-core/src/zsl/writer/postings.rs b/sdsearch-core/src/zsl/writer/postings.rs index e271609..1a6e42e 100644 --- a/sdsearch-core/src/zsl/writer/postings.rs +++ b/sdsearch-core/src/zsl/writer/postings.rs @@ -29,7 +29,7 @@ pub fn write_term_postings( let mut prev_pos = 0u32; for &pos in positions { - write_vint(prx, (pos - prev_pos) as u64); + write_vint(prx, u64::from(pos - prev_pos)); prev_pos = pos; } } diff --git a/sdsearch-core/src/zsl/writer/segments.rs b/sdsearch-core/src/zsl/writer/segments.rs index 0f37ac5..d175d9c 100644 --- a/sdsearch-core/src/zsl/writer/segments.rs +++ b/sdsearch-core/src/zsl/writer/segments.rs @@ -50,7 +50,7 @@ pub struct NewSegment { /// segment name for a counter (== Zend Lucene's segment-naming scheme). pub fn segment_name(counter: u32) -> String { - format!("_{}", to_base36(counter as u64)) + format!("_{}", to_base36(u64::from(counter))) } /// inverse of `to_base36`: parses a lowercase base-36 string to a number. `None` if any @@ -67,7 +67,7 @@ fn from_base36(s: &str) -> Option { b'a'..=b'z' => b - b'a' + 10, _ => return None, }; - n = n.checked_mul(36)?.checked_add(digit as u64)?; + n = n.checked_mul(36)?.checked_add(u64::from(digit))?; } Some(n) } @@ -86,9 +86,8 @@ fn from_base36(s: &str) -> Option { /// may have read the old `segments.gen` value an instant before the flip and are about to /// open the generation manifest it pointed to. fn prune_old_generations(index_dir: &Path, keep_from_gen: u64) { - let entries = match std::fs::read_dir(index_dir) { - Ok(e) => e, - Err(_) => return, + let Ok(entries) = std::fs::read_dir(index_dir) else { + return; }; for entry in entries.flatten() { let Some(name) = entry.file_name().to_str().map(str::to_string) else { diff --git a/sdsearch-core/src/zsl/writer/stored.rs b/sdsearch-core/src/zsl/writer/stored.rs index 531353d..a608c8d 100644 --- a/sdsearch-core/src/zsl/writer/stored.rs +++ b/sdsearch-core/src/zsl/writer/stored.rs @@ -38,7 +38,7 @@ impl StoredStreamWriter { write_vint(&mut block, fields.len() as u64); for sf in fields { write_vint(&mut block, sf.field_num as u64); - block.push(if sf.tokenized { 0x01 } else { 0x00 }); // never binary here + block.push(u8::from(sf.tokenized)); // never binary here write_modified_utf8(&mut block, &sf.value); } @@ -128,7 +128,7 @@ mod tests { write_vint(&mut fdt, fields.len() as u64); for sf in fields { write_vint(&mut fdt, sf.field_num as u64); - fdt.push(if sf.tokenized { 0x01 } else { 0x00 }); // never binary here + fdt.push(u8::from(sf.tokenized)); // never binary here write_modified_utf8(&mut fdt, &sf.value); } } @@ -203,7 +203,7 @@ mod tests { #[test] fn stored_raw_roundtrips_field_num_and_tokenized_flag() { - use crate::zsl::stored::{read_stored_raw, StoredRaw}; + use crate::zsl::stored::{StoredRaw, read_stored_raw}; let stored = vec![ vec![ StoredField { @@ -243,10 +243,12 @@ mod tests { ] ); // the host application does not index binaries: the round-trip must never report is_binary=true - assert!(read_stored_raw(&fdx, &fdt, 0) - .unwrap() - .iter() - .all(|r| !r.is_binary)); + assert!( + read_stored_raw(&fdx, &fdt, 0) + .unwrap() + .iter() + .all(|r| !r.is_binary) + ); assert_eq!( read_stored_raw(&fdx, &fdt, 1).unwrap(), vec![StoredRaw { diff --git a/sdsearch-core/src/zsl/writer/terms.rs b/sdsearch-core/src/zsl/writer/terms.rs index 42c44e1..31da40c 100644 --- a/sdsearch-core/src/zsl/writer/terms.rs +++ b/sdsearch-core/src/zsl/writer/terms.rs @@ -179,7 +179,7 @@ where let mut prev_pos = 0u32; for &pos in positions { - write_vint(&mut self.prx_scratch, (pos - prev_pos) as u64); + write_vint(&mut self.prx_scratch, u64::from(pos - prev_pos)); prev_pos = pos; } @@ -347,16 +347,13 @@ fn dump_entry( write_vint(out, prefix_chars as u64); write_modified_utf8(out, &text[prefix_bytes..]); write_vint(out, field_num as u64); - write_vint(out, doc_freq as u64); - match prev { - Some((_, _, pf, pp)) => { - write_vint(out, freq_ptr - pf); - write_vint(out, prox_ptr - pp); - } - None => { - write_vint(out, freq_ptr); - write_vint(out, prox_ptr); - } + write_vint(out, u64::from(doc_freq)); + if let Some((_, _, pf, pp)) = prev { + write_vint(out, freq_ptr - pf); + write_vint(out, prox_ptr - pp); + } else { + write_vint(out, freq_ptr); + write_vint(out, prox_ptr); } // skipOffset is omitted: docFreq is always < skipInterval. } @@ -577,7 +574,7 @@ mod tests { // state of the previous term in the .tis let mut prev: Option<(&str, usize, u64, u64)> = None; // (text, field, freqPtr, proxPtr) - // state of the last sample in the .tii + // state of the last sample in the .tii let mut idx_prev: Option<(&str, usize, u64, u64)> = None; let mut last_index_position: u64 = 24; @@ -637,16 +634,13 @@ mod tests { write_vint(out, prefix_chars as u64); write_modified_utf8(out, &term.text[prefix_bytes..]); write_vint(out, term.field_num as u64); - write_vint(out, doc_freq as u64); - match prev { - Some((_, _, pf, pp)) => { - write_vint(out, freq_ptr - pf); - write_vint(out, prox_ptr - pp); - } - None => { - write_vint(out, freq_ptr); - write_vint(out, prox_ptr); - } + write_vint(out, u64::from(doc_freq)); + if let Some((_, _, pf, pp)) = prev { + write_vint(out, freq_ptr - pf); + write_vint(out, prox_ptr - pp); + } else { + write_vint(out, freq_ptr); + write_vint(out, prox_ptr); } // skipOffset is omitted: docFreq is always < skipInterval. } diff --git a/sdsearch-core/tests/zsl_boolean_parity.rs b/sdsearch-core/tests/zsl_boolean_parity.rs index 1f30265..f547179 100644 --- a/sdsearch-core/tests/zsl_boolean_parity.rs +++ b/sdsearch-core/tests/zsl_boolean_parity.rs @@ -1,6 +1,6 @@ //! Parity of boolean composition (build_query + executor) vs the ZSL boolean oracle //! (a transcription of Zend Lucene's boolean query builder in the multiseg generator). -use sdsearch_core::query::{build_query, search, InGroup, Occur, QueryParams, WhereGroup}; +use sdsearch_core::query::{InGroup, Occur, QueryParams, WhereGroup, build_query, search}; use sdsearch_core::search::Hit; use sdsearch_core::zsl::index::ZslIndex; use std::collections::HashMap; @@ -31,7 +31,7 @@ fn idx() -> ZslIndex { .unwrap() } fn sorted(mut v: Vec) -> Vec { - v.sort(); + v.sort_unstable(); v } fn eng(hits: &[Hit]) -> Vec { diff --git a/sdsearch-core/tests/zsl_merge_memory_bound.rs b/sdsearch-core/tests/zsl_merge_memory_bound.rs index d52ff32..c851cf6 100644 --- a/sdsearch-core/tests/zsl_merge_memory_bound.rs +++ b/sdsearch-core/tests/zsl_merge_memory_bound.rs @@ -33,29 +33,35 @@ struct TrackingAlloc; unsafe impl GlobalAlloc for TrackingAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let p = System.alloc(layout); - if !p.is_null() { - let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); - PEAK.fetch_max(live, Ordering::Relaxed); + unsafe { + let p = System.alloc(layout); + if !p.is_null() { + let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); + PEAK.fetch_max(live, Ordering::Relaxed); + } + p } - p } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - System.dealloc(ptr, layout); - LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + unsafe { + System.dealloc(ptr, layout); + LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - let p = System.realloc(ptr, layout, new_size); - if !p.is_null() { - let old = layout.size(); - if new_size >= old { - let live = LIVE.fetch_add(new_size - old, Ordering::Relaxed) + (new_size - old); - PEAK.fetch_max(live, Ordering::Relaxed); - } else { - LIVE.fetch_sub(old - new_size, Ordering::Relaxed); + unsafe { + let p = System.realloc(ptr, layout, new_size); + if !p.is_null() { + let old = layout.size(); + if new_size >= old { + let live = LIVE.fetch_add(new_size - old, Ordering::Relaxed) + (new_size - old); + PEAK.fetch_max(live, Ordering::Relaxed); + } else { + LIVE.fetch_sub(old - new_size, Ordering::Relaxed); + } } + p } - p } } @@ -82,8 +88,7 @@ fn copy_kb_base() -> PathBuf { std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) + .map_or(0, |d| d.as_nanos()) )); if dst.is_dir() { std::fs::remove_dir_all(&dst).ok(); diff --git a/sdsearch-core/tests/zsl_multiseg_parity.rs b/sdsearch-core/tests/zsl_multiseg_parity.rs index 5714a29..3708188 100644 --- a/sdsearch-core/tests/zsl_multiseg_parity.rs +++ b/sdsearch-core/tests/zsl_multiseg_parity.rs @@ -1,7 +1,7 @@ //! Leaf-query parity over a real MULTI-SEGMENT ZSL index vs the ZSL oracle. //! Exercises global doc-ids crossing segments, deletion exclusion, and per-base routing. use sdsearch_core::index::IndexReader; -use sdsearch_core::search::{fuzzy_query, phrase_query, term_query, wildcard_query, Hit}; +use sdsearch_core::search::{Hit, fuzzy_query, phrase_query, term_query, wildcard_query}; use sdsearch_core::zsl::index::ZslIndex; use std::collections::HashMap; use std::path::PathBuf; @@ -38,7 +38,7 @@ fn idx() -> ZslIndex { .unwrap() } fn sorted(mut v: Vec) -> Vec { - v.sort(); + v.sort_unstable(); v } fn eng(hits: &[Hit]) -> Vec { diff --git a/sdsearch-core/tests/zsl_query_parity.rs b/sdsearch-core/tests/zsl_query_parity.rs index 85d48e7..0bc1958 100644 --- a/sdsearch-core/tests/zsl_query_parity.rs +++ b/sdsearch-core/tests/zsl_query_parity.rs @@ -32,12 +32,12 @@ fn seg() -> ZslSegment { fn doc_set(hits: &[sdsearch_core::search::Hit]) -> Vec { let mut v: Vec = hits.iter().map(|h| h.id).collect(); - v.sort(); + v.sort_unstable(); v } fn oracle_set(exp: &Expected, key: &str) -> Vec { let mut v: Vec = exp.queries[key].iter().map(|h| h.id).collect(); - v.sort(); + v.sort_unstable(); v } diff --git a/sdsearch-core/tests/zsl_query_parity_kb.rs b/sdsearch-core/tests/zsl_query_parity_kb.rs index b6bb9c8..4d641f6 100644 --- a/sdsearch-core/tests/zsl_query_parity_kb.rs +++ b/sdsearch-core/tests/zsl_query_parity_kb.rs @@ -31,7 +31,7 @@ fn seg() -> ZslSegment { .unwrap() } fn sorted(mut v: Vec) -> Vec { - v.sort(); + v.sort_unstable(); v } fn engine_set(hits: &[sdsearch_core::search::Hit]) -> Vec { diff --git a/sdsearch-core/tests/zsl_unclean_shutdown.rs b/sdsearch-core/tests/zsl_unclean_shutdown.rs index 54c78a5..9790c0e 100644 --- a/sdsearch-core/tests/zsl_unclean_shutdown.rs +++ b/sdsearch-core/tests/zsl_unclean_shutdown.rs @@ -40,7 +40,7 @@ fn opens_clean_with_orphan_cfs_and_stale_lock_file() { let existing_cfs = std::fs::read_dir(&dir) .unwrap() .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| p.extension().map(|x| x == "cfs").unwrap_or(false)) + .find(|p| p.extension().is_some_and(|x| x == "cfs")) .expect("the KB fixture has a .cfs"); std::fs::copy(&existing_cfs, dir.join("_99.cfs")).unwrap(); // orphan (not in segments_N) std::fs::write(dir.join("write.lock.file"), b"").unwrap(); // stale lock file (not held) diff --git a/sdsearch-php/src/lib.rs b/sdsearch-php/src/lib.rs index 0b6f7da..f317e5a 100644 --- a/sdsearch-php/src/lib.rs +++ b/sdsearch-php/src/lib.rs @@ -5,10 +5,10 @@ use ext_php_rs::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::Path; -use sdsearch_core::query::{build_query, search, InGroup, Occur, Query, QueryParams, WhereGroup}; +use sdsearch_core::query::{InGroup, Occur, Query, QueryParams, WhereGroup, build_query, search}; use sdsearch_core::zsl::index::ZslIndex; use sdsearch_core::zsl::runner::search_index; use sdsearch_core::zsl::writer::{IndexWriter, WriterDoc, WriterField, WriterOpts}; @@ -206,7 +206,7 @@ fn resolve_doc_id(index: &ZslIndex, id_field: &str, value: &str) -> Result:value` (the `field` is LITERAL, From 5f17796a179061cd9c63bb5a92a68367c9922865 Mon Sep 17 00:00:00 2001 From: Federico Martinez Date: Sun, 12 Jul 2026 10:21:00 -0300 Subject: [PATCH 2/2] ci: enforce workspace clippy lints + jscpd duplicate-code gate Two additions to raise the quality bar, both wired into the existing CI gates. Lints ([workspace.lints], inherited by both crates): enable clippy::pedantic as a baseline plus rustc unsafe_op_in_unsafe_fn=deny (matters for the FFI/mmap/ GlobalAlloc unsafe) and unreachable_pub. The pedantic lints that are pure noise on a hand-rolled binary-format parser (intentional narrowing casts, exact-float test asserts, lowercase-only file-extension compares, doc-prose nags) are allow-listed with a documented rationale rather than churned away. CI already runs `cargo clippy --all-targets -- -D warnings`, so this is a hard gate. jscpd: copy-paste detector as a regression ratchet. Threshold 7% in .jscpd.json sits above the current ~4.9% (which is mostly intentional: the #[cfg(test)] differential-oracle encoder and repetitive FFI wrappers), so it does not force refactoring today but fails CI on a large new duplicate. Runs in the lint job (node is preinstalled on the runner). --- .github/workflows/ci.yml | 4 +++ .jscpd.json | 9 +++++++ Cargo.toml | 55 ++++++++++++++++++++++++++++++++++++++++ sdsearch-core/Cargo.toml | 3 +++ sdsearch-php/Cargo.toml | 3 +++ 5 files changed, 74 insertions(+) create mode 100644 .jscpd.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8324524..a02648f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,10 @@ jobs: - run: cargo audit - run: cargo deny check - run: cargo machete + # Copy-paste detection (regression ratchet): fails if duplication in the crate sources + # exceeds the threshold in .jscpd.json. Node is preinstalled on ubuntu runners. + - name: Duplicate-code check (jscpd) + run: npx --yes jscpd sdsearch-core/src sdsearch-php/src coverage: name: Coverage (sdsearch-core -> octocov) diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000..a5cb3ef --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/kucherenko/jscpd/master/packages/jscpd/schema.json", + "threshold": 7, + "minTokens": 50, + "reporters": ["console"], + "gitignore": true, + "absolute": true, + "ignore": ["**/target/**"] +} diff --git a/Cargo.toml b/Cargo.toml index a4ddef3..0d44460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,58 @@ members = ["sdsearch-core", "sdsearch-php"] strip = "symbols" # drop the symbol table: smaller binary, no internal-name leakage lto = "fat" # whole-program LTO: maximum optimization (build time is not a concern) codegen-units = 1 # let the optimizer work across the whole crate + +# Workspace-wide lint policy, inherited by each member via `[lints] workspace = true`. +# CI runs `cargo clippy --all-targets -- -D warnings`, so every `warn` here is effectively a +# hard gate. Levels are curated to be high-signal on THIS codebase (an FFI extension with +# unsafe + heavy hand-rolled byte parsing), not the full pedantic firehose. +[workspace.lints.rust] +# In an `unsafe fn`, still require an explicit `unsafe {}` around each unsafe op — the FFI / +# mmap / GlobalAlloc code is exactly where a silently-unsafe body hides a real hazard. +unsafe_op_in_unsafe_fn = "deny" +# `pub` items not actually reachable from the crate's public API should be `pub(crate)`. +unreachable_pub = "warn" + +[workspace.lints.clippy] +# pedantic as a baseline (priority -1 so the specific `allow`s below win over it), then opt OUT +# of the pedantic lints that are noise on THIS codebase instead of fixing hundreds of call sites. +pedantic = { level = "warn", priority = -1 } + +# Intentional narrowing/among-format casts. This crate hand-decodes a binary index format where +# `read_vint(...) as usize`, `len as u32`, etc. are deliberate and bounds-are-checked elsewhere; +# rewriting every one as TryFrom would bloat and slow the hot parse paths for no real safety win. +cast_possible_truncation = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +cast_possible_wrap = "allow" + +# Doc-prose nags with high false-positive rates on our comments (bare `.tis`, `ZSL`, `TermInfo`, +# etc. flagged as needing backticks; every fallible/panicking internal fn asked for a section). +doc_markdown = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" + +# Structural nags that fight the existing (deliberate) style of this codebase. +too_many_lines = "allow" +similar_names = "allow" +items_after_statements = "allow" +implicit_hasher = "allow" + +# Exact float compares live only in tests asserting known table values (encode_norm, length_norm, +# a zero score) — an exact `==` is precisely what those want. +float_cmp = "allow" +# Segment file extensions are always written lowercase by our own writer, so the case-sensitive +# `ends_with(".tis")` comparisons are correct and intentional. +case_sensitive_file_extension_comparisons = "allow" +# Consumer/builder APIs (e.g. `IndexReader::add_document(self, doc: Document)`) deliberately take +# ownership even when the current body doesn't move every field — that is the intended contract. +needless_pass_by_value = "allow" +# `&Option` is a fine signature for an internal helper; not worth rippling through call sites. +ref_option = "allow" +# JSON usage examples in doc comments (`{"fields":[...]}`) trip the intra-doc-link heuristic. +doc_link_with_quotes = "allow" + +# leftover debugging that must never reach a release. +todo = "warn" +dbg_macro = "warn" diff --git a/sdsearch-core/Cargo.toml b/sdsearch-core/Cargo.toml index 8c7fef6..77e7692 100644 --- a/sdsearch-core/Cargo.toml +++ b/sdsearch-core/Cargo.toml @@ -9,6 +9,9 @@ authors = ["InvGate"] keywords = ["lucene", "search", "zend", "php", "index"] categories = ["text-processing"] +[lints] +workspace = true + [lib] name = "sdsearch_core" diff --git a/sdsearch-php/Cargo.toml b/sdsearch-php/Cargo.toml index ae12d27..ae8987f 100644 --- a/sdsearch-php/Cargo.toml +++ b/sdsearch-php/Cargo.toml @@ -9,6 +9,9 @@ authors = ["InvGate"] keywords = ["lucene", "search", "zend", "php", "index"] categories = ["text-processing"] +[lints] +workspace = true + [lib] name = "sdsearch" crate-type = ["cdylib"]