From e09fe85963b12246c49b76fe7c47ddd8f4383374 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 03:27:33 +0000 Subject: [PATCH 1/4] feat: reinstate split command as a first-class operation Restores the split command that was absorbed into lookup -s. The key behavioral difference: split() always runs the greedy segmentation algorithm directly, while lookup -s tries an exact match first and only splits as a fallback. Changes: - lib: add SplitOptions struct (lib/src/core/split.rs) - lib: add perform_split() kernel and split() method to lookup! macro; refactor perform_lookup() to delegate to perform_split() for deduplication - lib: fix byte-boundary bug from original split implementation (now uses char count instead of byte length for threshold comparison) - lib: add split tests (lib/tests/split.rs) - cli: add odict split command with -m/--min-length, -F, -i, -r flags - serve: add GET /{name}/split HTTP endpoint - node: add SplitOptions type and split() method on OpenDictionary - node: add split tests - python: add SplitOptions type and split() method on OpenDictionary - python: add split tests https://claude.ai/code/session_01VQ1ponG9b5YivzuUY9cJnd --- cli/src/cli.rs | 6 +- cli/src/lib.rs | 2 + cli/src/main.rs | 3 +- cli/src/serve/mod.rs | 2 + cli/src/serve/split.rs | 104 +++++++++++++++++++++++++++++++ cli/src/split.rs | 102 ++++++++++++++++++++++++++++++ lib/src/core/lookup.rs | 99 ++++++++++++++++++----------- lib/src/core/mod.rs | 1 + lib/src/core/split.rs | 36 +++++++++++ lib/tests/split.rs | 50 +++++++++++++++ node/__test__/dictionary.spec.ts | 28 +++++++++ node/src/node.rs | 29 ++++++++- node/src/types/mod.rs | 2 + node/src/types/split.rs | 37 +++++++++++ python/src/dictionary.rs | 40 +++++++++++- python/src/lib.rs | 3 +- python/src/types/mod.rs | 2 + python/src/types/split.rs | 57 +++++++++++++++++ python/tests/test_split.py | 52 ++++++++++++++++ 19 files changed, 614 insertions(+), 41 deletions(-) create mode 100644 cli/src/serve/split.rs create mode 100644 cli/src/split.rs create mode 100644 lib/src/core/split.rs create mode 100644 lib/tests/split.rs create mode 100644 node/src/types/split.rs create mode 100644 python/src/types/split.rs create mode 100644 python/tests/test_split.py diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 1b18528c5..9e509030a 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -3,7 +3,7 @@ use clap::{command, crate_version, Parser, Subcommand}; use crate::alias::AliasCommands; use crate::{ CompileArgs, DownloadArgs, DumpArgs, IndexArgs, InfoArgs, LexiconArgs, LookupArgs, MergeArgs, - NewArgs, SearchArgs, ServeArgs, TokenizeArgs, + NewArgs, SearchArgs, ServeArgs, SplitArgs, TokenizeArgs, }; #[derive(Debug, Parser)] @@ -71,6 +71,10 @@ pub enum Commands { #[command(arg_required_else_help = true)] Serve(ServeArgs), + /// Splits text into component dictionary words without attempting a whole-word lookup first + #[command(arg_required_else_help = true)] + Split(SplitArgs), + /// Tokenize text and find dictionary entries for each token #[command(arg_required_else_help = true)] Tokenize(TokenizeArgs), diff --git a/cli/src/lib.rs b/cli/src/lib.rs index c461e1f9c..25797d291 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -15,6 +15,7 @@ mod new; mod print; mod search; mod serve; +mod split; mod tokenize; mod utils; @@ -34,5 +35,6 @@ pub use new::*; pub use print::*; pub use search::*; pub use serve::*; +pub use split::*; pub use tokenize::*; pub use utils::*; diff --git a/cli/src/main.rs b/cli/src/main.rs index 0de76be81..1b99323c2 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; use console::style; use odict_cli::{ - alias, compile, download, dump, index, info, lexicon, lookup, merge, new, search, serve, + alias, compile, download, dump, index, info, lexicon, lookup, merge, new, search, serve, split, tokenize, CLIContext, Commands, CLI, }; @@ -23,6 +23,7 @@ async fn main() { Commands::Search(ref args) => search(&mut ctx, args).await, Commands::Serve(ref args) => serve(&mut ctx, args).await, Commands::Info(ref args) => info(&mut ctx, args).await, + Commands::Split(ref args) => split(&mut ctx, args).await, Commands::Tokenize(ref args) => tokenize(&mut ctx, args).await, }; diff --git a/cli/src/serve/mod.rs b/cli/src/serve/mod.rs index 74d822b98..08d3a2369 100644 --- a/cli/src/serve/mod.rs +++ b/cli/src/serve/mod.rs @@ -18,6 +18,7 @@ use crate::CLIContext; mod lookup; mod search; +mod split; mod tokenize; #[derive(Debug, Clone, ValueEnum)] @@ -174,6 +175,7 @@ pub async fn serve<'a>(ctx: &mut CLIContext<'a>, args: &ServeArgs) -> anyhow::Re .app_data(Data::clone(&data)) .service(lookup::handle_lookup) .service(search::handle_search) + .service(split::handle_split) .service(tokenize::handle_tokenize) }) .bind(("0.0.0.0", *port))? diff --git a/cli/src/serve/split.rs b/cli/src/serve/split.rs new file mode 100644 index 000000000..5026b6573 --- /dev/null +++ b/cli/src/serve/split.rs @@ -0,0 +1,104 @@ +use actix_web::{ + get, + http::{header::ContentType, StatusCode}, + web::{Data, Path, Query}, + HttpResponse, Responder, ResponseError, +}; +use derive_more::{Display, Error}; +use odict::{format::json::ToJSON, split::SplitOptions}; +use serde::Deserialize; + +use crate::get_lookup_entries; + +#[derive(Debug, Deserialize)] +pub struct SplitRequest { + q: String, + follow: Option, + min_length: Option, +} + +#[derive(Debug, Display, Error)] +enum SplitError { + #[display("Dictionary not found: {}", name)] + DictionaryNotFound { name: String }, + + #[display("Failed to read dictionary: {}", name)] + DictionaryReadError { name: String }, + + #[display("Split error: {}", message)] + SplitError { message: String }, + + #[display("Failed to serialize response")] + SerializeError, +} + +impl ResponseError for SplitError { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()) + .insert_header(ContentType::html()) + .body(self.to_string()) + } + + fn status_code(&self) -> StatusCode { + match *self { + SplitError::DictionaryNotFound { .. } => StatusCode::NOT_FOUND, + SplitError::DictionaryReadError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + SplitError::SplitError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + SplitError::SerializeError => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[get("/{name}/split")] +async fn handle_split( + params: Query, + dict: Path, + dictionary_cache: Data, +) -> Result { + let SplitRequest { + q: raw_queries, + follow, + min_length, + } = params.0; + + let queries = raw_queries + .split(',') + .map(|s| s.to_string()) + .collect::>(); + + let dictionary_name = dict.into_inner(); + + let file = dictionary_cache + .get(&dictionary_name) + .await + .map_err(|_e| SplitError::DictionaryReadError { + name: dictionary_name.to_string(), + })? + .ok_or(SplitError::DictionaryNotFound { + name: dictionary_name.to_string(), + })?; + + let dictionary = file + .contents() + .map_err(|_e| SplitError::DictionaryReadError { + name: dictionary_name.to_string(), + })?; + + let opts = SplitOptions::default() + .threshold(min_length.unwrap_or(1)) + .follow(follow.unwrap_or(false)); + + let entries = dictionary + .split(&queries, &opts) + .map_err(|e| SplitError::SplitError { + message: e.to_string(), + })?; + + let json = get_lookup_entries(entries) + .to_json(true) + .map_err(|_e| SplitError::SerializeError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(json)) +} diff --git a/cli/src/split.rs b/cli/src/split.rs new file mode 100644 index 000000000..a795a1f1e --- /dev/null +++ b/cli/src/split.rs @@ -0,0 +1,102 @@ +use std::time::Duration; + +use crate::enums::PrintFormat; +use crate::get_lookup_entries; +use crate::{context::CLIContext, print_entries}; +use clap::{arg, command, Args}; +use odict::{ + download::DictionaryDownloader, + split::SplitOptions, + LoadOptions, OpenDictionary, +}; + +#[derive(Debug, Args)] +#[command(args_conflicts_with_subcommands = true)] +#[command(flatten_help = true)] +pub struct SplitArgs { + #[arg(required = true, help = "Path to a compiled dictionary")] + dictionary_path: String, + + #[arg(required = true, help = "Text to split into dictionary words")] + queries: Vec, + + #[arg( + short, + long, + value_enum, + default_value_t = PrintFormat::Print, + help = "Output format of the entries" + )] + format: PrintFormat, + + #[arg( + short = 'F', + long, + help = "Follow see_also redirects until finding an entry with etymologies" + )] + follow: bool, + + #[arg( + short = 'm', + long, + default_value_t = 1, + help = "Minimum character length of each split segment" + )] + min_length: usize, + + #[arg( + short = 'i', + long, + default_value_t = false, + help = "Perform case-insensitive lookups" + )] + insensitive: bool, + + #[arg( + short = 'r', + long, + default_value_t = crate::DEFAULT_RETRIES, + help = "Number of times to retry loading the dictionary (remote-only)" + )] + retries: u32, +} + +pub async fn split<'a>(ctx: &mut CLIContext<'a>, args: &SplitArgs) -> anyhow::Result<()> { + let SplitArgs { + dictionary_path: path, + queries, + format, + follow, + min_length, + insensitive, + retries, + } = args; + + let spinner = indicatif::ProgressBar::new_spinner(); + + spinner.enable_steady_tick(Duration::from_millis(100)); + + let file = OpenDictionary::load_with_options( + path, + LoadOptions::default() + .with_downloader(DictionaryDownloader::default().with_retries(*retries)), + ) + .await?; + + let opts = SplitOptions::default() + .threshold(*min_length) + .follow(*follow) + .insensitive(*insensitive); + + let result = file.contents()?.split(queries, opts); + + spinner.finish_and_clear(); + + match result { + Ok(entries) => { + print_entries(ctx, get_lookup_entries(entries), format)?; + Ok(()) + } + Err(err) => Err(anyhow::Error::from(err)), + } +} diff --git a/lib/src/core/lookup.rs b/lib/src/core/lookup.rs index db23ab446..13a8a9f39 100644 --- a/lib/src/core/lookup.rs +++ b/lib/src/core/lookup.rs @@ -132,6 +132,47 @@ macro_rules! lookup { Ok($opt::None) } + fn perform_split<'a>( + &'a self, + query: &str, + options: &crate::split::SplitOptions, + ) -> crate::Result>> { + let crate::split::SplitOptions { + threshold, + follow, + insensitive, + } = options; + + let chars: Vec<_> = query.chars().collect(); + let mut results: Vec> = Vec::new(); + let mut start = 0; + let mut end = chars.len(); + + while start < end { + let substr: String = chars[start..end].iter().collect(); + let mut path = Vec::new(); + + match self.find_entry(follow, insensitive, substr.as_str(), None, &mut path) { + Ok($opt::Some(result)) => { + results.push(result); + start = end; + end = chars.len(); + } + Ok($opt::None) => { + if end - start <= *threshold { + start = end; + end = chars.len(); + } else { + end -= 1; + } + } + Err(e) => return Err(e), + } + } + + Ok(results) + } + fn perform_lookup<'a, Options>( &'a self, query: &str, @@ -154,46 +195,32 @@ macro_rules! lookup { return Ok(vec![result]); } - let mut results: Vec> = Vec::new(); - if let LookupStrategy::Split(min_length) = strategy { - let chars: Vec<_> = query.chars().collect(); - let mut start = 0; - let mut end = chars.len(); - - while start < end { - let substr: String = chars[start..end].iter().collect(); - let mut substr_path = Vec::new(); - let maybe_entry = self.find_entry( - follow, - insensitive, - substr.as_str(), - None, - &mut substr_path, - ); - - match maybe_entry { - Ok($opt::Some(result)) => { - results.push(result); - start = end; - end = chars.len(); - continue; - } - Ok($opt::None) => { - if substr.len() <= *min_length { - start = end; - end = chars.len(); - continue; - } - } - Err(e) => return Err(e), - } + let split_opts = crate::split::SplitOptions::default() + .threshold(*min_length) + .follow(*follow) + .insensitive(*insensitive); - end -= 1; - } + return self.perform_split(query, &split_opts); } - Ok(results) + Ok(vec![]) + } + + pub fn split<'a, Query, Options>( + &'a self, + queries: &Vec, + options: Options, + ) -> crate::Result>> + where + Query: AsRef + Send + Sync, + Options: AsRef + Send + Sync, + { + queries + .par_iter() + .map(|q| self.perform_split(q.as_ref(), options.as_ref())) + .collect::>>() + .map(|v| v.into_iter().flatten().collect()) } pub fn lookup<'a, 'b, Query, Options>( diff --git a/lib/src/core/mod.rs b/lib/src/core/mod.rs index 976addb31..1e305ebbc 100644 --- a/lib/src/core/mod.rs +++ b/lib/src/core/mod.rs @@ -4,6 +4,7 @@ pub mod compile; pub mod lexicon; pub mod lookup; pub mod merge; +pub mod split; pub mod preview; pub mod rank; pub mod read; diff --git a/lib/src/core/split.rs b/lib/src/core/split.rs new file mode 100644 index 000000000..f2926c1d4 --- /dev/null +++ b/lib/src/core/split.rs @@ -0,0 +1,36 @@ +pub struct SplitOptions { + pub threshold: usize, + pub follow: bool, + pub insensitive: bool, +} + +impl AsRef for SplitOptions { + fn as_ref(&self) -> &Self { + self + } +} + +impl SplitOptions { + pub fn default() -> Self { + Self { + threshold: 1, + follow: false, + insensitive: false, + } + } + + pub fn threshold(mut self, threshold: usize) -> Self { + self.threshold = threshold; + self + } + + pub fn follow(mut self, follow: bool) -> Self { + self.follow = follow; + self + } + + pub fn insensitive(mut self, insensitive: bool) -> Self { + self.insensitive = insensitive; + self + } +} diff --git a/lib/tests/split.rs b/lib/tests/split.rs new file mode 100644 index 000000000..f403ad691 --- /dev/null +++ b/lib/tests/split.rs @@ -0,0 +1,50 @@ +mod helpers; + +#[cfg(test)] +mod split_tests { + + use odict::split::SplitOptions; + + use crate::helpers::EXAMPLE_DICT_1; + + #[test] + fn test_split() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + let result = dict + .split(&vec!["catdog"], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].entry.term, "cat"); + assert_eq!(result[1].entry.term, "dog"); + } + + #[test] + fn test_split_with_numbers() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + let result = dict + .split(&vec!["2cat8dog"], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].entry.term, "cat"); + assert_eq!(result[1].entry.term, "dog"); + } + + #[test] + fn test_split_does_not_attempt_whole_word_lookup() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + // "cat" exists as a whole word in the dictionary. + // split() should still find it via the segmentation path, not a short-circuit exact match. + // The result should be identical regardless of which path is taken. + let result = dict + .split(&vec!["cat"], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].entry.term, "cat"); + } +} diff --git a/node/__test__/dictionary.spec.ts b/node/__test__/dictionary.spec.ts index 2fd4b2cd2..1c539b6fc 100644 --- a/node/__test__/dictionary.spec.ts +++ b/node/__test__/dictionary.spec.ts @@ -78,6 +78,34 @@ test('lookup - supports follow=false to disable following', async (t) => { t.is(result[0].entry.term, 'ran') }) +test('split - splits compound text into dictionary words', async (t) => { + const result = dict1.split('catdog') + t.is(result.length, 2) + t.is(result[0].entry.term, 'cat') + t.is(result[1].entry.term, 'dog') +}) + +test('split - does not attempt whole-word lookup first', async (t) => { + // 'catdog' does not exist as a whole word; split should still find 'cat' and 'dog' + const lookupResult = dict1.lookup('catdog') + t.is(lookupResult.length, 0) + + const splitResult = dict1.split('catdog') + t.is(splitResult.length, 2) +}) + +test('split - returns single word when it matches a dictionary entry', async (t) => { + const result = dict1.split('cat') + t.is(result.length, 1) + t.is(result[0].entry.term, 'cat') +}) + +test('split - respects minLength option', async (t) => { + const result = dict1.split('catdog', { minLength: 4 }) + // 'cat' is only 3 chars, so with minLength=4 it won't be found as a segment + t.is(result.length, 0) +}) + test('can return the lexicon', async (t) => { const result = dict1.lexicon() t.deepEqual(result, ['cat', 'dog', 'poo', 'ran', 'run']) diff --git a/node/src/node.rs b/node/src/node.rs index 739cfa1ac..5da42be31 100644 --- a/node/src/node.rs +++ b/node/src/node.rs @@ -4,7 +4,7 @@ use crate::types::LoadOptions; use crate::types::SaveOptions; use crate::{ shared::compile, - types::{self, Entry}, + types::{self, Entry, SplitOptions}, utils::cast_error, }; @@ -81,6 +81,33 @@ impl OpenDictionary { crate::shared::perform_lookup(&self.dict, query, options) } + #[napi] + pub fn split( + &self, + query: Either>, + options: Option, + ) -> Result> { + let mut queries: Vec = vec![]; + + match query { + Either::A(a) => queries.push(a.into()), + Either::B(mut c) => queries.append(&mut c), + } + + let dict = self.dict.contents().map_err(cast_error)?; + + let opts = odict::split::SplitOptions::from(options.unwrap_or_default()); + + let results = dict.split(&queries, &opts).map_err(cast_error)?; + + let mapped = results + .iter() + .map(|result| result.try_into()) + .collect::>>()?; + + Ok(mapped) + } + #[napi] pub fn lexicon(&self) -> Result> { crate::shared::get_lexicon(&self.dict) diff --git a/node/src/types/mod.rs b/node/src/types/mod.rs index d3856afd8..65a52e697 100644 --- a/node/src/types/mod.rs +++ b/node/src/types/mod.rs @@ -19,6 +19,7 @@ mod note; mod pronunciation; mod search; mod sense; +mod split; mod token; mod tokenize; mod translation; @@ -35,6 +36,7 @@ pub use index::IndexOptions; pub use lookup::{LookupOptions, LookupResult}; pub use pronunciation::Pronunciation; pub use search::SearchOptions; +pub use split::SplitOptions; pub use token::Token; pub use tokenize::TokenizeOptions; pub use translation::Translation; diff --git a/node/src/types/split.rs b/node/src/types/split.rs new file mode 100644 index 000000000..4d707bbca --- /dev/null +++ b/node/src/types/split.rs @@ -0,0 +1,37 @@ +#[napi(object)] +#[derive(Clone)] +pub struct SplitOptions { + pub min_length: Option, + pub follow: Option, + pub insensitive: Option, +} + +impl Default for SplitOptions { + fn default() -> Self { + SplitOptions { + min_length: None, + follow: None, + insensitive: None, + } + } +} + +impl From for odict::split::SplitOptions { + fn from(opts: SplitOptions) -> Self { + let mut options = odict::split::SplitOptions::default(); + + if let Some(min_length) = opts.min_length { + options = options.threshold(min_length as usize); + } + + if let Some(follow) = opts.follow { + options = options.follow(follow); + } + + if let Some(insensitive) = opts.insensitive { + options = options.insensitive(insensitive); + } + + options + } +} diff --git a/python/src/dictionary.rs b/python/src/dictionary.rs index 993a331f0..7aa411375 100644 --- a/python/src/dictionary.rs +++ b/python/src/dictionary.rs @@ -5,7 +5,10 @@ use pyo3_async_runtimes::tokio::future_into_py; use odict::ToDictionary; use crate::{ - types::{Entry, IndexOptions, LoadOptions, LookupOptions, LookupResult, SearchOptions, Token}, + types::{ + Entry, IndexOptions, LoadOptions, LookupOptions, LookupResult, SearchOptions, SplitOptions, + Token, + }, utils::cast_error, }; @@ -135,6 +138,41 @@ impl OpenDictionary { Ok(mapped) } + #[pyo3(signature = (query, min_length=None, follow=None, insensitive=None))] + pub fn split( + &self, + query: Either>, + min_length: Option, + follow: Option, + insensitive: Option, + ) -> PyResult> { + let mut queries: Vec = vec![]; + + match query { + Either::Left(a) => queries.push(a), + Either::Right(mut c) => queries.append(&mut c), + } + + let options = SplitOptions { + min_length, + follow, + insensitive, + }; + + let dict = self.dict.contents().map_err(cast_error)?; + + let results = dict + .split(&queries, &odict::split::SplitOptions::from(options)) + .map_err(cast_error)?; + + let mapped = results + .iter() + .map(|result| LookupResult::from_archive(result)) + .collect::, _>>()?; + + Ok(mapped) + } + pub fn lexicon(&self) -> PyResult> { let dict = self.dict.contents().map_err(cast_error)?; let lexicon = dict.lexicon(); diff --git a/python/src/lib.rs b/python/src/lib.rs index ef56fb9fd..fecb6cba2 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -2,7 +2,7 @@ use dictionary::{compile, OpenDictionary}; use pyo3::prelude::*; use types::{ CompressOptions, EnumWrapper, IndexOptions, LoadOptions, LookupOptions, LookupResult, MediaURL, - Pronunciation, RemoteLoadOptions, SaveOptions, SearchOptions, TokenizeOptions, + Pronunciation, RemoteLoadOptions, SaveOptions, SearchOptions, SplitOptions, TokenizeOptions, }; mod dictionary; @@ -24,6 +24,7 @@ fn theopendictionary(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/python/src/types/mod.rs b/python/src/types/mod.rs index 0842b4468..3b5ed0879 100644 --- a/python/src/types/mod.rs +++ b/python/src/types/mod.rs @@ -14,6 +14,7 @@ mod pronunciation; mod save; mod search; mod sense; +mod split; mod token; mod tokenize; mod translation; @@ -28,6 +29,7 @@ pub use media_url::*; pub use pronunciation::*; pub use save::*; pub use search::*; +pub use split::*; pub use token::*; pub use tokenize::*; pub use translation::*; diff --git a/python/src/types/split.rs b/python/src/types/split.rs new file mode 100644 index 000000000..48499292c --- /dev/null +++ b/python/src/types/split.rs @@ -0,0 +1,57 @@ +use pyo3::prelude::*; + +#[pyclass] +#[derive(Clone)] +pub struct SplitOptions { + #[pyo3(get, set)] + pub min_length: Option, + + #[pyo3(get, set)] + pub follow: Option, + + #[pyo3(get, set)] + pub insensitive: Option, +} + +#[pymethods] +impl SplitOptions { + #[new] + #[pyo3(signature = (min_length=None, follow=None, insensitive=None))] + pub fn new(min_length: Option, follow: Option, insensitive: Option) -> Self { + SplitOptions { + min_length, + follow, + insensitive, + } + } +} + +impl Default for SplitOptions { + fn default() -> Self { + SplitOptions { + min_length: None, + follow: None, + insensitive: None, + } + } +} + +impl From for odict::split::SplitOptions { + fn from(opts: SplitOptions) -> Self { + let mut options = odict::split::SplitOptions::default(); + + if let Some(min_length) = opts.min_length { + options = options.threshold(min_length as usize); + } + + if let Some(follow) = opts.follow { + options = options.follow(follow); + } + + if let Some(insensitive) = opts.insensitive { + options = options.insensitive(insensitive); + } + + options + } +} diff --git a/python/tests/test_split.py b/python/tests/test_split.py new file mode 100644 index 000000000..769fa0372 --- /dev/null +++ b/python/tests/test_split.py @@ -0,0 +1,52 @@ +import pytest + +from pathlib import Path +from theopendictionary import OpenDictionary, compile + + +@pytest.fixture(scope="module") +def dict1(): + current_file = Path(__file__).resolve() + xml_path = current_file.parent.parent.parent / "examples" / "example1.xml" + with open(xml_path, "r") as f: + xml_content = f.read() + compiled_bytes = compile(xml_content) + return OpenDictionary(compiled_bytes) + + +def test_split_compound_into_words(dict1): + result = dict1.split("catdog") + assert len(result) == 2 + assert result[0].entry.term == "cat" + assert result[1].entry.term == "dog" + + +def test_split_does_not_attempt_whole_word_lookup(dict1): + # 'catdog' does not exist in the dictionary — lookup returns nothing + lookup_result = dict1.lookup("catdog") + assert len(lookup_result) == 0 + + # split() finds the components regardless + split_result = dict1.split("catdog") + assert len(split_result) == 2 + + +def test_split_single_word(dict1): + result = dict1.split("cat") + assert len(result) == 1 + assert result[0].entry.term == "cat" + + +def test_split_with_min_length(dict1): + # min_length=4 means segments shorter than 4 chars are skipped + # 'cat' is 3 chars, 'dog' is 3 chars — neither qualifies + result = dict1.split("catdog", min_length=4) + assert len(result) == 0 + + +def test_split_with_numbers_in_text(dict1): + # Non-dictionary characters between words should be skipped + result = dict1.split("2cat8dog") + assert len(result) == 2 + assert result[0].entry.term == "cat" + assert result[1].entry.term == "dog" From 54a04e067dd650e904fbc6c6565ae844ce2b2345 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 04:06:29 +0000 Subject: [PATCH 2/4] test(lib): expand split tests from 3 to 11 for comprehensive coverage Adds tests for: no matches, empty string, threshold behavior, multiple queries with order preservation, follow=false, follow=true (with directed_from verification), case-sensitive default, and case-insensitive mode. https://claude.ai/code/session_01VQ1ponG9b5YivzuUY9cJnd --- lib/tests/split.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/lib/tests/split.rs b/lib/tests/split.rs index f403ad691..ef9d72309 100644 --- a/lib/tests/split.rs +++ b/lib/tests/split.rs @@ -39,7 +39,6 @@ mod split_tests { // "cat" exists as a whole word in the dictionary. // split() should still find it via the segmentation path, not a short-circuit exact match. - // The result should be identical regardless of which path is taken. let result = dict .split(&vec!["cat"], SplitOptions::default()) .unwrap(); @@ -47,4 +46,112 @@ mod split_tests { assert_eq!(result.len(), 1); assert_eq!(result[0].entry.term, "cat"); } + + #[test] + fn test_split_no_matches() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + let result = dict + .split(&vec!["xyz"], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 0); + } + + #[test] + fn test_split_empty_string() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + let result = dict + .split(&vec![""], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 0); + } + + #[test] + fn test_split_threshold_skips_short_segments() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + // "cat" and "dog" are each 3 chars; threshold=4 means segments of ≤4 chars + // are discarded when no match is found, so neither word is ever tried at length 3 + let result = dict + .split(&vec!["catdog"], SplitOptions::default().threshold(4)) + .unwrap(); + + assert_eq!(result.len(), 0); + } + + #[test] + fn test_split_multiple_queries_preserves_order() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + // Results from each query are appended in input order + let result = dict + .split(&vec!["catdog", "dogcat"], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 4); + assert_eq!(result[0].entry.term, "cat"); + assert_eq!(result[1].entry.term, "dog"); + assert_eq!(result[2].entry.term, "dog"); + assert_eq!(result[3].entry.term, "cat"); + } + + #[test] + fn test_split_without_follow() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + // "ran" has a see_also pointing to "run"; without follow, "ran" is returned as-is + let result = dict + .split(&vec!["randog"], SplitOptions::default().follow(false)) + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].entry.term, "ran"); + assert!(result[0].directed_from.is_none()); + assert_eq!(result[1].entry.term, "dog"); + } + + #[test] + fn test_split_with_follow() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + // "ran" see_also "run"; with follow=true, the split should resolve to "run" + let result = dict + .split(&vec!["randog"], SplitOptions::default().follow(true)) + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].entry.term, "run"); + assert_eq!(result[0].directed_from.unwrap().term, "ran"); + assert_eq!(result[1].entry.term, "dog"); + } + + #[test] + fn test_split_case_sensitive_by_default() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + // "CAT" does not match "cat" with default (case-sensitive) settings; + // only "dog" (lowercase) should be found + let result = dict + .split(&vec!["CATdog"], SplitOptions::default()) + .unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].entry.term, "dog"); + } + + #[test] + fn test_split_case_insensitive() { + let dict = EXAMPLE_DICT_1.contents().unwrap(); + + let result = dict + .split(&vec!["CATdog"], SplitOptions::default().insensitive(true)) + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].entry.term, "cat"); + assert_eq!(result[1].entry.term, "dog"); + } } From 49121d33f1361ad0b4702da8dbd661d9014bdccb Mon Sep 17 00:00:00 2001 From: Tyler Nickerson Date: Fri, 20 Mar 2026 09:58:19 -0400 Subject: [PATCH 3/4] lint --- crates/cli/src/split.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/cli/src/split.rs b/crates/cli/src/split.rs index a795a1f1e..068687fca 100644 --- a/crates/cli/src/split.rs +++ b/crates/cli/src/split.rs @@ -3,12 +3,8 @@ use std::time::Duration; use crate::enums::PrintFormat; use crate::get_lookup_entries; use crate::{context::CLIContext, print_entries}; -use clap::{arg, command, Args}; -use odict::{ - download::DictionaryDownloader, - split::SplitOptions, - LoadOptions, OpenDictionary, -}; +use clap::Args; +use odict::{download::DictionaryDownloader, split::SplitOptions, LoadOptions, OpenDictionary}; #[derive(Debug, Args)] #[command(args_conflicts_with_subcommands = true)] From 1581b95818237625dd9d54452c55cf0e59ba5d4d Mon Sep 17 00:00:00 2001 From: Tyler Nickerson Date: Fri, 20 Mar 2026 09:59:17 -0400 Subject: [PATCH 4/4] update lockfile --- Cargo.lock | 297 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 287 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32ece0db8..bc471cb90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -379,6 +379,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -565,6 +587,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -675,12 +703,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.15.11" @@ -725,6 +772,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "core2" version = "0.4.0" @@ -1138,6 +1201,12 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -1348,6 +1417,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fst" version = "0.4.7" @@ -2070,6 +2145,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -2246,7 +2343,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "tar", - "thiserror", + "thiserror 2.0.18", "tokio", "yada", ] @@ -2611,7 +2708,7 @@ dependencies = [ "strum 0.28.0", "tantivy", "tempfile", - "thiserror", + "thiserror 2.0.18", "tokio", "url", "uuid", @@ -2687,6 +2784,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -3056,7 +3159,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.1", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3068,6 +3171,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -3077,7 +3181,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3244,7 +3348,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -3350,8 +3454,13 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3373,7 +3482,7 @@ dependencies = [ "async-trait", "http 1.4.0", "reqwest 0.13.2", - "thiserror", + "thiserror 2.0.18", "tower-service", ] @@ -3392,7 +3501,7 @@ dependencies = [ "reqwest 0.13.2", "reqwest-middleware", "retry-policies", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "wasmtimer", @@ -3492,6 +3601,7 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -3500,6 +3610,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.13.1" @@ -3510,12 +3632,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3542,6 +3692,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3569,7 +3728,30 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3918,7 +4100,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror", + "thiserror 2.0.18", "time", "uuid", "winapi", @@ -4048,13 +4230,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -4592,6 +4794,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.4" @@ -4648,6 +4859,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4684,6 +4904,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4717,6 +4952,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4729,6 +4970,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4741,6 +4988,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4765,6 +5018,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4777,6 +5036,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4789,6 +5054,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4801,6 +5072,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6"