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/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/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-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/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"] 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,