diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..81cc4ef --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,14 @@ +# Consider adding "--codegen=link-args=-Wl,--compress-debug-sections=zlib" + +[target.x86_64-unknown-linux-gnu] +# SSE3 is requred by simd-varint. +# POPCNT makes `count_ones` (which we use in geofilter and bitrank) more efficient. +rustflags = ["-C", "target-feature=+ssse3,+avx2,+popcnt"] + +[target.x86_64-apple-darwin] +# SSE3 is requred by simd-varint. +# POPCNT makes `count_ones` (which we use in geofilter and bitrank) more efficient. +rustflags = ["-C", "target-feature=+ssse3,+avx2,+popcnt"] + +[target.aarch64-apple-darwin] +rustflags = ["-C", "target-feature=+neon"] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5331725..113b3a6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,9 @@ name: CI on: push: + branches: [ main ] + pull_request: + workflow_dispatch: permissions: contents: read @@ -18,21 +21,24 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: rui314/setup-mold@b015f7e3f2938ad3a5ed6e5111a8c6c7c1d6db6e + - uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d - name: Build run: make build + - name: Build JS + run: make build-js + lint: name: Lint runs-on: ubuntu-latest needs: build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: rui314/setup-mold@b015f7e3f2938ad3a5ed6e5111a8c6c7c1d6db6e + - uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d - name: Check formatting and clippy run: make lint @@ -41,9 +47,9 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: rui314/setup-mold@b015f7e3f2938ad3a5ed6e5111a8c6c7c1d6db6e + - uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d - name: Run unit tests run: make test diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..1552d29 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,29 @@ +name: Publish + +on: + workflow_dispatch: # Allow manual triggering of the workflow + +permissions: + contents: read + +jobs: + publish-npm: + runs-on: ubuntu-latest + defaults: + run: + working-directory: crates/string-offsets/js + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org/ + cache: npm + cache-dependency-path: crates/string-offsets/js/package-lock.json + - run: npm ci + - run: npm run compile + - run: npm test + - run: echo "Publishing string-offsets" + - run: npm whoami; npm --ignore-scripts publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.gitignore b/.gitignore index 0654041..4bb94c0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock /crates/*/target/ /crates/*/Cargo.lock .vscode/ +.DS_Store diff --git a/Makefile b/Makefile index 862425b..770e8f3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: all -all: build lint test +all: build build-js lint test .PHONY: clean clean: @@ -22,6 +22,11 @@ build: # Use --all-targets to ensure that all of the benchmarks compile. cargo build --all-targets --all-features +.PHONY: build-js +build-js: + npm --prefix crates/string-offsets/js install + npm --prefix crates/string-offsets/js run compile + .PHONY: test test: RUST_BACKTRACE=1 cargo test diff --git a/README.md b/README.md index a97abe3..ae3acce 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A collection of useful algorithms written in Rust. Currently contains: - [`geo_filters`](crates/geo_filters): probabilistic data structures that solve the [Distinct Count Problem](https://en.wikipedia.org/wiki/Count-distinct_problem) using geometric filters. - [`bpe`](crates/bpe): fast, correct, and novel algorithms for the [Byte Pair Encoding Algorithm](https://en.wikipedia.org/wiki/Large_language_model#BPE) which are particularly useful for chunking of documents. +- [`bpe-openai`](crates/bpe-openai): Fast tokenizers for OpenAI token sets based on the `bpe` crate. - [`string-offsets`](crates/string-offsets): converts string positions between bytes, chars, UTF-16 code units, and line numbers. Useful when sending string indices across language boundaries. ## Background diff --git a/crates/bpe-openai/Cargo.toml b/crates/bpe-openai/Cargo.toml index 3f35ede..c20ee2c 100644 --- a/crates/bpe-openai/Cargo.toml +++ b/crates/bpe-openai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bpe-openai" -version = "0.2.1" +version = "0.3.0" edition = "2021" description = "Prebuilt fast byte-pair encoders for OpenAI." repository = "https://github.com/github/rust-gems" @@ -13,18 +13,19 @@ crate-type = ["lib", "staticlib"] bench = false [dependencies] -bpe = { version = "0.2.0", path = "../bpe" } +bpe = { version = "0.2", path = "../bpe" } either = "1.13" regex-automata = "0.4" rmp-serde = "1" +unicode-normalization = "0.1" [dev-dependencies] -bpe = { version = "0.2.0", path = "../bpe", features = ["rand"] } -tiktoken-rs = "0.6" +bpe = { version = "0.2", path = "../bpe", features = ["rand"] } +tiktoken-rs = "0.7" [build-dependencies] -base64 = "0.22.1" -bpe = { version = "0.2.0", path = "../bpe", features = ["tiktoken"] } +base64 = "0.22" +bpe = { version = "0.2", path = "../bpe", features = ["tiktoken"] } flate2 = "1.0" rmp-serde = "1" serde = "1" diff --git a/crates/bpe-openai/build.rs b/crates/bpe-openai/build.rs index 528eae6..6976e91 100644 --- a/crates/bpe-openai/build.rs +++ b/crates/bpe-openai/build.rs @@ -17,6 +17,11 @@ fn main() { include_bytes!("data/o200k_base.tiktoken.gz"), 17846336922010275747, ); + serialize_tiktoken_bpe( + "voyage3_base", + include_bytes!("data/voyage3_base.tiktoken.gz"), + 17846336922010275747, + ); println!("cargo::rerun-if-changed=build.rs"); } diff --git a/crates/bpe-openai/data/voyage3_base.tiktoken.gz b/crates/bpe-openai/data/voyage3_base.tiktoken.gz new file mode 100644 index 0000000..96b3bc6 Binary files /dev/null and b/crates/bpe-openai/data/voyage3_base.tiktoken.gz differ diff --git a/crates/bpe-openai/src/lib.rs b/crates/bpe-openai/src/lib.rs index 385749e..51f514f 100644 --- a/crates/bpe-openai/src/lib.rs +++ b/crates/bpe-openai/src/lib.rs @@ -8,6 +8,11 @@ use regex_automata::{ Anchored, Input, }; +pub mod normalizer; + +pub use bpe::*; +pub use normalizer::{Normalizable, NormalizedString}; + // Note: Below we rewrite the negative look-ahead with a positive pseudo look-ahead. // The look-ahead character is dropped from the match by the Pretokenizer iterator. // Note: The negative look-ahead `\\s+(?!\\S)` requires `\\s+\\s` but also `\\s+$` to handle end of file without dropping a character! @@ -18,7 +23,7 @@ static BPE_CL100K_BASE: LazyLock = LazyLock::new(|| { let pat1 = "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+$"; let pat2 = "\\s+\\s"; let pat3 = "\\s+"; - Tokenizer::new_lookahead(bpe, &[(pat1, false), (pat2, true), (pat3, false)]) + Tokenizer::new_lookahead(bpe, &[(pat1, false), (pat2, true), (pat3, false)], false) .expect("valid regex") }); @@ -35,11 +40,19 @@ static BPE_O200K_BASE: LazyLock = LazyLock::new(|| { ].join("|"); let pat2 = "\\s+\\s"; let pat3 = "\\s+"; - Tokenizer::new_lookahead(bpe, &[(&pat1, false), (pat2, true), (pat3, false)]) + Tokenizer::new_lookahead(bpe, &[(&pat1, false), (pat2, true), (pat3, false)], false) .expect("valid regex") }); -pub use bpe::*; +static BPE_VOYAGE3_BASE: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/bpe_voyage3_base.dict")); + let bpe = rmp_serde::from_slice(bytes).expect("valid bpe data"); + let pat1 = "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+$"; + let pat2 = "\\s+\\s"; + let pat3 = "\\s+"; + Tokenizer::new_lookahead(bpe, &[(pat1, false), (pat2, true), (pat3, false)], true) + .expect("valid regex") +}); /// A byte-pair encoding tokenizer that supports a pre-tokenization regex. /// The direct methods on this type pre-tokenize the input text and should @@ -52,6 +65,8 @@ pub struct Tokenizer { pub bpe: BytePairEncoding, /// The pattern regex used to split the input. pub pre: Option, + /// Indicates whether the input should be normalized with NFC. + nfc: bool, } pub struct Pretokenizer { @@ -64,9 +79,9 @@ pub struct Pretokenizer { impl Tokenizer { /// Build a tokenizer with an optional pretokenization regex pattern. #[allow(clippy::result_large_err)] - pub fn new(bpe: BytePairEncoding, pat: Option<&str>) -> Result { + pub fn new(bpe: BytePairEncoding, pat: Option<&str>, nfc: bool) -> Result { let pre = pat.map(Pretokenizer::new).transpose()?; - Ok(Self { bpe, pre }) + Ok(Self { nfc, bpe, pre }) } /// Build a tokenizer with pretokenization regex patterns. If the boolean for a pattern is true, @@ -75,15 +90,17 @@ impl Tokenizer { pub fn new_lookahead( bpe: BytePairEncoding, patterns: &[(&str, bool)], + nfc: bool, ) -> Result { let pre = Some(Pretokenizer::new_lookahead(patterns)?); - Ok(Self { bpe, pre }) + Ok(Self { nfc, bpe, pre }) } /// Count the number of tokens produced when encoding the text. Applies pre-tokenization /// before counting. - pub fn count(&self, text: &str) -> usize { - self.split(text) + pub fn count<'a, I: Normalizable<'a>>(&self, text: I) -> usize { + let text = self.normalize(text); + self.split(text.as_str()) .map(|piece| self.bpe.count(piece.as_bytes())) .sum() } @@ -91,18 +108,23 @@ impl Tokenizer { /// Returns the token count iff the total token count stays below the specified token_limit. /// Otherwise, it returns none. This function can be faster than [`Self::count`]` when the /// token limit is much smaller than the provided text. Applies pre-tokenization before counting. - pub fn count_till_limit(&self, text: &str, token_limit: usize) -> Option { - self.split(text).try_fold(0, |consumed, piece| { + /// + /// Note: This function assumes that the text is already normalized, so that this function can run + /// in roughly O(token_limit) time. + pub fn count_till_limit(&self, text: &NormalizedString, token_limit: usize) -> Option { + let res: Option = self.split(text.as_str()).try_fold(0, |consumed, piece| { self.bpe .count_till_limit(piece.as_bytes(), token_limit - consumed) .map(|piece_count| consumed + piece_count) - }) + }); + res } /// Returns the tokens for the encoding of the given text. Applies pre-tokenization before /// encoding. - pub fn encode(&self, text: &str) -> Vec { - self.split(text) + pub fn encode<'a, I: Normalizable<'a>>(&self, text: I) -> Vec { + let text: NormalizedString<'_> = self.normalize(text); + self.split(text.as_str()) .flat_map(|piece| self.bpe.encode_via_backtracking(piece.as_bytes())) .collect() } @@ -114,12 +136,18 @@ impl Tokenizer { /// Returns an iterator with the text pieces resulting from pre-tokenization. If this /// tokenizer does not have pre-tokenization, the iterator returns the full text. - pub fn split<'a>(&'a self, text: &'a str) -> impl Iterator + 'a { + pub fn split<'a>(&'a self, text: &'a str) -> impl Iterator { match &self.pre { Some(pre) => Either::Left(pre.split(text)), None => Either::Right(std::iter::once(text)), } } + + /// Returns the normalized text if the tokenizer requires normalization. + /// If the input was already normalized, this function is a noop. + pub fn normalize<'a, I: Normalizable<'a>>(&self, text: I) -> NormalizedString<'a> { + text.normalize(self.nfc) + } } impl Pretokenizer { @@ -143,7 +171,7 @@ impl Pretokenizer { } /// Returns an iterator with the text pieces after splitting with the regular expression. - pub fn split<'a>(&'a self, text: &'a str) -> impl Iterator + 'a { + pub fn split<'a>(&'a self, text: &'a str) -> impl Iterator { Splits { pat: &self.pat, lookahead: &self.lookahead, @@ -201,6 +229,10 @@ pub fn o200k_base() -> &'static Tokenizer { &BPE_O200K_BASE } +pub fn voyage3_base() -> &'static Tokenizer { + &BPE_VOYAGE3_BASE +} + #[cfg(test)] mod tests { use bpe::byte_pair_encoding::{create_test_string, select_test_string}; @@ -210,12 +242,12 @@ mod tests { #[test] fn test_cl100k() { - test_equivalence(cl100k_base(), &cl100k_base_singleton().lock()); + test_equivalence(cl100k_base(), cl100k_base_singleton()); } #[test] fn test_o200k() { - test_equivalence(o200k_base(), &o200k_base_singleton().lock()); + test_equivalence(o200k_base(), o200k_base_singleton()); } #[track_caller] @@ -233,9 +265,21 @@ mod tests { #[test] fn test_count_till_limit() { - assert_eq!(cl100k_base().count_till_limit("abc", 3), Some(1)); - assert_eq!(cl100k_base().count_till_limit("abcabc", 3), Some(2)); - assert_eq!(cl100k_base().count_till_limit("abcabcabc", 3), Some(3)); - assert_eq!(cl100k_base().count_till_limit("abcabcabcabc", 3), None); + assert_eq!( + cl100k_base().count_till_limit(&cl100k_base().normalize("abc"), 3), + Some(1) + ); + assert_eq!( + cl100k_base().count_till_limit(&cl100k_base().normalize("abcabc"), 3), + Some(2) + ); + assert_eq!( + cl100k_base().count_till_limit(&cl100k_base().normalize("abcabcabc"), 3), + Some(3) + ); + assert_eq!( + cl100k_base().count_till_limit(&cl100k_base().normalize("abcabcabcabc"), 3), + None + ); } } diff --git a/crates/bpe-openai/src/normalizer.rs b/crates/bpe-openai/src/normalizer.rs new file mode 100644 index 0000000..50f3309 --- /dev/null +++ b/crates/bpe-openai/src/normalizer.rs @@ -0,0 +1,58 @@ +use std::borrow::Cow; + +use unicode_normalization::UnicodeNormalization; + +/// Type which represents a normalized string. +/// This is to avoid calling normalize multiple times or forgetting to call normalization! +/// +/// TODO: Annotate the type with the normalization type, once there are more than one. +pub struct NormalizedString<'a>(Cow<'a, str>); + +impl<'a> NormalizedString<'a> { + /// Returns the normalized inner str buffer. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// This function is unsafe, since the caller must ensure that the correct normalization + /// was used. The normalization may vary by tokenizer. This mostly a backdoor which might + /// be handy for certain optimizations or for testing. + /// + /// # Safety + /// This is safe if `s` is in fact correctly normalized already. The caller is + /// responsible for ensuring that. + pub unsafe fn from_str(s: &'a str) -> NormalizedString<'a> { + NormalizedString(Cow::Borrowed(s)) + } +} + +/// Helper trait which converts string types into NormalizedString. +/// Calling normalize on a NormalizedString is a no-op. +pub trait Normalizable<'a> { + fn normalize(self, nfc: bool) -> NormalizedString<'a>; +} + +impl<'a> Normalizable<'a> for &'a str { + fn normalize(self, nfc: bool) -> NormalizedString<'a> { + if nfc { + NormalizedString(self.nfc().collect()) + } else { + NormalizedString(Cow::Borrowed(self)) + } + } +} + +impl<'a, T> Normalizable<'a> for &'a T +where + T: AsRef, +{ + fn normalize(self, nfc: bool) -> NormalizedString<'a> { + self.as_ref().normalize(nfc) + } +} + +impl<'a> Normalizable<'a> for NormalizedString<'a> { + fn normalize(self, _: bool) -> NormalizedString<'a> { + self + } +} diff --git a/crates/bpe/Cargo.toml b/crates/bpe/Cargo.toml index d9dff96..fdf26aa 100644 --- a/crates/bpe/Cargo.toml +++ b/crates/bpe/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bpe" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "Fast byte-pair encoding implementation." repository = "https://github.com/github/rust-gems" @@ -20,13 +20,13 @@ tiktoken = ["dep:base64"] aneubeck-daachorse = "1.1.1" base64 = { version = "0.22", optional = true } fnv = "1.0" -itertools = "0.12" -rand = { version = "0.8", optional = true } +itertools = "0.14" +rand = { version = "0.9", optional = true } serde = { version = "1", features = ["derive"] } [dev-dependencies] bpe = { path = "." } -tiktoken-rs = "0.6" +tiktoken-rs = "0.7" [package.metadata.docs.rs] all-features = true diff --git a/crates/bpe/benchmarks/Cargo.toml b/crates/bpe/benchmarks/Cargo.toml index 87f35c2..09f0788 100644 --- a/crates/bpe/benchmarks/Cargo.toml +++ b/crates/bpe/benchmarks/Cargo.toml @@ -18,9 +18,9 @@ path = "equivalence.rs" test = true [dependencies] -bpe = { path = "../../bpe" } +bpe = { path = "../../bpe", features = ["rand", "tiktoken"] } bpe-openai = { path = "../../bpe-openai" } -criterion = "0.5" -rand = "0.8" -tiktoken-rs = "0.6" +criterion = "0.7" +rand = "0.9" +tiktoken-rs = "0.7" tokenizers = { version = "0.21", features = ["http"] } diff --git a/crates/bpe/benchmarks/equivalence.rs b/crates/bpe/benchmarks/equivalence.rs index b3df973..4019602 100644 --- a/crates/bpe/benchmarks/equivalence.rs +++ b/crates/bpe/benchmarks/equivalence.rs @@ -1,19 +1,71 @@ +use std::collections::HashSet; + use bpe::byte_pair_encoding::{create_test_string, select_test_string}; use bpe_benchmarks::*; -#[cfg(test)] -const N: usize = 32; +/// Converts bytes to unicode characters. +/// See https://github.com/openai/gpt-2/blob/master/src/encoder.py#L9 +/// Hugging face uses the same mapping to work with unicode instead of byte characters. +fn char_to_byte(c: char) -> u8 { + match c as u32 { + 0x21..0x7f => c as u8, // 94 + 0xa1..=0xac => c as u8, // 12 + 0xae..=0xff => c as u8, // 82 + 0x7f..0xa1 => c as u8 - 0x7f + 221, + 0x100..0x121 => (c as u32 - 0x100) as u8, + 0x121..0x143 => (c as u32 - 0x121) as u8 + 0x7f, + 0x143..0x144 => 0xad, + _ => panic!("Invalid character: {c} {}", c as u32), + } +} #[test] -fn test_huggingface_encoding_equivalence_without_pretokenization() { - for (_, bpe, _, huggingface) in TOKENIZERS.iter() { +fn test_compare_dictionary() { + for (name, bpe, _, huggingface) in TOKENIZERS.iter() { let huggingface = without_pretokenizer(huggingface); - let text = create_test_string(&bpe.bpe, 80_000); - let texts = (0..N) - .map(|_| select_test_string(&text, 100)) + let mut hugging_tokens = huggingface.get_vocab(false); + // HACK: There are incorrect vocabularies in huggingface which have the added tokens stored together with the base tokens.. + // This is a workaround to remove them. + for added_token in huggingface.get_added_vocabulary().get_vocab().keys() { + hugging_tokens.remove(added_token); + } + let mut hugging_tokens: Vec<_> = hugging_tokens.into_iter().collect(); + hugging_tokens.sort_by(|(_, a), (_, b)| a.cmp(b)); + let hugging_tokens: Vec<_> = hugging_tokens + .into_iter() + .map(|(token, _)| token.chars().map(char_to_byte).collect()) + .collect(); + let bpe_tokens: Vec<_> = (0..bpe.bpe.num_tokens()) + .map(|id| bpe.bpe.token_bytes(id as u32).to_vec()) + .collect(); + let hugging_set: HashSet<_> = hugging_tokens.iter().cloned().collect(); + let bpe_set: HashSet<_> = bpe_tokens.iter().cloned().collect(); + let diff: Vec<_> = hugging_set.symmetric_difference(&bpe_set).collect(); + assert!(diff.is_empty(), "{name}: Token sets differ"); + // Uncomment the following lines to write the tokens to a file in tiktoken format + /* + let mut file = + std::fs::File::create(std::path::Path::new(_name)).expect("can create output file"); + std::io::Write::write_all( + &mut file, + bpe::byte_pair_encoding::write_tiktoken(hugging_tokens).as_bytes(), + ) + .expect("can write output to file"); + */ + } +} + +#[test] +fn test_huggingface_encoding_equivalence_without_pretokenization() { + for (name, bpe, _, huggingface) in TOKENIZERS.iter() { + let text: String = create_test_string(&bpe.bpe, 200_000); + let text = bpe.normalize(&text); + let texts = (0..300) + .map(|_| select_test_string(text.as_str(), 100)) .chain(std::iter::once( "You should see the Greek word 'kosme': \"κόσμε\"", )); + let huggingface = without_pretokenizer(huggingface); for text in texts { let out = bpe.bpe.encode_via_backtracking(text.as_bytes()); let huggingface_out = huggingface @@ -26,14 +78,10 @@ fn test_huggingface_encoding_equivalence_without_pretokenization() { let huggingface_text = huggingface.decode(&huggingface_out, true).unwrap(); if huggingface_text != text { panic!( - "huggingface tokens and text differ: {:?} != {:?}", - text, huggingface_text + "{name}: huggingface tokens and text differ: {text:?} != {huggingface_text:?}", ); } else { - panic!( - "huggingface tokens differ: {:?} != {:?}", - out, huggingface_out - ); + panic!("{name}: huggingface tokens differ: {out:?} != {huggingface_out:?}"); } } } @@ -42,9 +90,9 @@ fn test_huggingface_encoding_equivalence_without_pretokenization() { #[test] fn test_huggingface_encoding_equivalence_with_pretokenization() { - for (_, bpe, _, huggingface) in TOKENIZERS.iter() { - let text = create_test_string(&bpe.bpe, 80_000); - let texts = (0..N) + for (name, bpe, _, huggingface) in TOKENIZERS.iter() { + let text = create_test_string(&bpe.bpe, 200_000); + let texts = (0..300) .map(|_| select_test_string(&text, 100)) .chain(std::iter::once( "You should see the Greek word 'kosme': \"κόσμε\" ", @@ -62,14 +110,10 @@ fn test_huggingface_encoding_equivalence_with_pretokenization() { let huggingface_text = huggingface.decode(&huggingface_out, true).unwrap(); if huggingface_text != text { panic!( - "huggingface tokens and text differ: {:?} != {:?}", - text, huggingface_text + "{name}: huggingface tokens and text differ: {text:?} != {huggingface_text:?}", ); } else { - panic!( - "huggingface tokens differ: {:?} != {:?}", - out, huggingface_out - ); + panic!("{name}: huggingface tokens differ: {out:?} != {huggingface_out:?}"); } } } diff --git a/crates/bpe/benchmarks/lib.rs b/crates/bpe/benchmarks/lib.rs index d364df8..00ffd4a 100644 --- a/crates/bpe/benchmarks/lib.rs +++ b/crates/bpe/benchmarks/lib.rs @@ -5,27 +5,35 @@ use tiktoken_rs::CoreBPE as TiktokenTokenizer; use tokenizers::pre_tokenizers::byte_level::ByteLevel as HuggingfaceByteLevel; use tokenizers::tokenizer::Tokenizer as HuggingfaceTokenizer; +#[allow(clippy::type_complexity)] pub static TOKENIZERS: LazyLock< [( &'static str, &'static Tokenizer, - TiktokenTokenizer, + Option, HuggingfaceTokenizer, - ); 2], + ); 3], > = LazyLock::new(|| { [ ( "cl100k", bpe_openai::cl100k_base(), - tiktoken_rs::cl100k_base().expect("tokenizer available"), + Some(tiktoken_rs::cl100k_base().expect("tokenizer available")), HuggingfaceTokenizer::from_pretrained("Xenova/gpt-4", None).expect("model available"), ), ( "o200k", bpe_openai::o200k_base(), - tiktoken_rs::o200k_base().expect("tokenizer available"), + Some(tiktoken_rs::o200k_base().expect("tokenizer available")), HuggingfaceTokenizer::from_pretrained("Xenova/gpt-4o", None).expect("model available"), ), + ( + "voyage3", + bpe_openai::voyage3_base(), + None, + HuggingfaceTokenizer::from_pretrained("voyageai/voyage-code-3", None) + .expect("model available"), + ), ] }); diff --git a/crates/bpe/benchmarks/performance.rs b/crates/bpe/benchmarks/performance.rs index d192225..4259498 100644 --- a/crates/bpe/benchmarks/performance.rs +++ b/crates/bpe/benchmarks/performance.rs @@ -9,7 +9,7 @@ use bpe_benchmarks::*; use criterion::{ criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration, }; -use rand::{thread_rng, Rng}; +use rand::{rng, Rng}; fn counting_benchmark(c: &mut Criterion) { for (name, bpe, _, _) in TOKENIZERS.iter() { @@ -22,7 +22,7 @@ fn counting_benchmark(c: &mut Criterion) { group.throughput(criterion::Throughput::Bytes(bytes as u64)); group.bench_with_input(BenchmarkId::new("interval", bytes), &bytes, |b, bytes| { b.iter_batched( - || thread_rng().gen_range(0..input.len() - bytes), + || rng().random_range(0..input.len() - bytes), |start| fast.count(start..start + bytes), criterion::BatchSize::SmallInput, ) @@ -32,7 +32,7 @@ fn counting_benchmark(c: &mut Criterion) { &bytes, |b, bytes| { b.iter_batched( - || thread_rng().gen_range(0..input.len() - bytes), + || rng().random_range(0..input.len() - bytes), |start| bpe.bpe.count(&input.as_bytes()[start..start + bytes]), criterion::BatchSize::SmallInput, ) @@ -163,13 +163,15 @@ fn comparison_benchmark(c: &mut Criterion) { ) }, ); - group.bench_with_input(BenchmarkId::new("tiktoken", bytes), &bytes, |b, bytes| { - b.iter_batched( - || select_test_string(&text, *bytes), - |text| tiktoken.encode_ordinary(text), - criterion::BatchSize::SmallInput, - ) - }); + if let Some(tiktoken) = tiktoken { + group.bench_with_input(BenchmarkId::new("tiktoken", bytes), &bytes, |b, bytes| { + b.iter_batched( + || select_test_string(&text, *bytes), + |text| tiktoken.encode_ordinary(text), + criterion::BatchSize::SmallInput, + ) + }); + } group.bench_with_input( BenchmarkId::new("huggingface", bytes), &bytes, @@ -206,13 +208,15 @@ fn worstcase_comparison_benchmark(c: &mut Criterion) { ) }, ); - group.bench_with_input(BenchmarkId::new("tiktoken", bytes), &bytes, |b, bytes| { - b.iter_batched( - || select_test_string(&text, *bytes), - |text| tiktoken.encode_ordinary(text), - criterion::BatchSize::SmallInput, - ) - }); + if let Some(tiktoken) = tiktoken { + group.bench_with_input(BenchmarkId::new("tiktoken", bytes), &bytes, |b, bytes| { + b.iter_batched( + || select_test_string(&text, *bytes), + |text| tiktoken.encode_ordinary(text), + criterion::BatchSize::SmallInput, + ) + }); + } group.bench_with_input( BenchmarkId::new("huggingface", bytes), &bytes, diff --git a/crates/bpe/src/bitfield.rs b/crates/bpe/src/bitfield.rs index e90002e..4a0b5fe 100644 --- a/crates/bpe/src/bitfield.rs +++ b/crates/bpe/src/bitfield.rs @@ -8,7 +8,7 @@ impl BitField { /// All bits are initialized to 1. pub(crate) fn new(bits: usize) -> Self { Self { - bitfield: vec![u64::MAX; (bits + 63) / 64], + bitfield: vec![u64::MAX; bits.div_ceil(64)], } } diff --git a/crates/bpe/src/byte_pair_encoding.rs b/crates/bpe/src/byte_pair_encoding.rs index 9c5a014..495e6ad 100644 --- a/crates/bpe/src/byte_pair_encoding.rs +++ b/crates/bpe/src/byte_pair_encoding.rs @@ -171,9 +171,9 @@ pub fn find_hash_factor_for_dictionary(tokens: impl IntoIterator> use rand::Rng; let all_tokens = tokens.into_iter().collect_vec(); - let mut rnd = rand::thread_rng(); + let mut rnd = rand::rng(); loop { - let factor: u64 = rnd.gen(); + let factor: u64 = rnd.random(); let mut seen = HashSet::new(); if all_tokens .iter() @@ -568,7 +568,7 @@ pub fn create_test_string_with_predicate( min_bytes: usize, predicate: impl Fn(&str) -> bool, ) -> String { - use rand::{thread_rng, Rng}; + use rand::{rng, Rng}; // the string we accumulated thus far let mut result = String::new(); // the tokens we added so we can backtrack @@ -577,7 +577,7 @@ pub fn create_test_string_with_predicate( // try a few times to find a suitable token 'next: for _ in 0..8 { // pick a random token and provisionally add it - let i = thread_rng().gen_range(0..bpe.num_tokens()) as u32; + let i = rng().random_range(0..bpe.num_tokens()) as u32; // We only use tokens that are valid UTF-8. This is true for ~99% of tokens in OpenAI's // token set. The chance of constructing a valid UTF-8 character across a token boundary // by picking random tokens is so small that it is unlikely to happen anyway. @@ -603,8 +603,8 @@ pub fn create_test_string_with_predicate( #[cfg(feature = "rand")] pub fn select_test_string(text: &str, min_bytes: usize) -> &str { - use rand::{thread_rng, Rng}; - let mut start = thread_rng().gen_range(0..text.len() - min_bytes); + use rand::{rng, Rng}; + let mut start = rng().random_range(0..text.len() - min_bytes); while !text.is_char_boundary(start) { start -= 1; } @@ -618,10 +618,10 @@ pub fn select_test_string(text: &str, min_bytes: usize) -> &str { /// Generate test bytes by concatenating random tokens. #[cfg(feature = "rand")] pub fn create_test_bytes(bpe: &BytePairEncoding, min_bytes: usize) -> Vec { - use rand::{thread_rng, Rng}; + use rand::{rng, Rng}; let mut result = Vec::new(); while result.len() < min_bytes { - let i = thread_rng().gen_range(0..bpe.num_tokens()); + let i = rng().random_range(0..bpe.num_tokens()); result.extend(bpe.token_bytes(i as u32)); } result diff --git a/crates/bpe/tests/Cargo.toml b/crates/bpe/tests/Cargo.toml index dcfed3e..e061e95 100644 --- a/crates/bpe/tests/Cargo.toml +++ b/crates/bpe/tests/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" [dependencies] bpe = { path = "../../bpe", features = ["rand"] } bpe-openai = { path = "../../bpe-openai" } -itertools = "0.13" -rand = "0.8" -tiktoken-rs = "0.6" +itertools = "0.14" +rand = "0.9" +tiktoken-rs = "0.7" diff --git a/crates/bpe/tests/src/lib.rs b/crates/bpe/tests/src/lib.rs index eccb548..6ea1322 100644 --- a/crates/bpe/tests/src/lib.rs +++ b/crates/bpe/tests/src/lib.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use itertools::Itertools; - use rand::{thread_rng, Rng}; + use rand::{rng, Rng}; use tiktoken_rs::cl100k_base_singleton; use bpe::appendable_encoder::AppendableEncoder; @@ -89,7 +89,6 @@ mod tests { .unwrap(); let bpe = &cl100k_base().bpe; let encoded1 = cl100k_base_singleton() - .lock() .encode_ordinary(input) .into_iter() .collect_vec(); @@ -122,8 +121,8 @@ mod tests { let input = create_test_bytes(bpe, 10000); let intervals = IntervalEncoding::new(bpe, &input); for _ in 0..1000 { - let start = thread_rng().gen_range(0..input.len()); - let end = thread_rng().gen_range(0..input.len()); + let start = rng().random_range(0..input.len()); + let end = rng().random_range(0..input.len()); let range = start.min(end)..start.max(end); assert_eq!( intervals.count(range.clone()), diff --git a/crates/geo_filters/Cargo.toml b/crates/geo_filters/Cargo.toml index a1e596c..18b6784 100644 --- a/crates/geo_filters/Cargo.toml +++ b/crates/geo_filters/Cargo.toml @@ -14,6 +14,8 @@ bench = false [features] default = [] +test-support = ["dep:rand", "dep:rand_chacha"] +serde = ["dep:serde"] evaluation = [ "dep:clap", "dep:hyperloglogplus", @@ -26,17 +28,19 @@ evaluation = [ clap = { version = "4", optional = true, features = ["derive"] } fnv = "1.0" hyperloglogplus = { version = "0.4", optional = true } -itertools = "0.13" +itertools = "0.14" once_cell = "1.18" -rand = { version = "0.8", optional = true } +rand = { version = "0.9", optional = true } rayon = { version = "1.7", optional = true } regex = { version = "1", optional = true } +serde = { version = "1.0", default-features = false, optional = true } +rand_chacha = { version = "0.9", optional = true } [dev-dependencies] -criterion = "0.5" +criterion = "0.7" geo_filters = { path = ".", features = ["evaluation"] } -rand = "0.8" -rand_chacha = "0.3" +rand = "0.9" +rand_chacha = "0.9" rayon = "1.7" [[bench]] diff --git a/crates/geo_filters/README.md b/crates/geo_filters/README.md index 69cf4f3..8b5ebf2 100644 --- a/crates/geo_filters/README.md +++ b/crates/geo_filters/README.md @@ -33,8 +33,7 @@ c2.push(2); c2.push(3); let estimated_size = c1.size_with_sketch(&c2); -assert!(estimated_size >= 3.0_f32 * 0.9 && - estimated_size <= 3.0_f32 * 1.1); +assert_eq!(estimated_size, 3); ``` ## Background diff --git a/crates/geo_filters/docs/choosing-a-hash-function.md b/crates/geo_filters/docs/choosing-a-hash-function.md new file mode 100644 index 0000000..5115eeb --- /dev/null +++ b/crates/geo_filters/docs/choosing-a-hash-function.md @@ -0,0 +1,115 @@ +# Choosing a hash function + +## Reproducibility + +This library uses hash functions to assign values to buckets deterministically. The same item +will hash to the same value, and modify the same bit in the geofilter. + +When comparing geofilters it is important that the same hash functions, using the same seed +values, have been used for *both* filters. Attempting to compare geofilters which have been +produced using different hash functions or the same hash function with different seeds will +produce nonsensical results. + +Similar to the Rust standard library, this crate uses the `BuildHasher` trait and creates +a new `Hasher` for every item processed. + +To help prevent mistakes caused by mismatching hash functions or seeds we introduce a trait +`ReproducibleBuildHasher` which you must implement if you wish to use a custom hashing function. +By marking a `BuildHasher` with this trait you're asserting that `Hasher`s produced using +`Default::default` will hash identical items to the same `u64` value across multiple calls +to `BuildHasher::hash_one`. + +The following is an example of some incorrect code which produces nonsense results: + +```rust +use std::hash::RandomState; + +// Implement our marker trait for `RandomState`. +// You should _NOT_ do this as `RandomState::default` does not produce +// reproducible hashers. +impl ReproducibleBuildHasher for RandomState {} + +#[test] +fn test_different_hash_functions() { + // The last parameter in this FixedConfig means we're using RandomState as the BuildHasher + pub type FixedConfigRandom = FixedConfig; + + let mut a = GeoDiffCount::new(FixedConfigRandom::default()); + let mut b = GeoDiffCount::new(FixedConfigRandom::default()); + + // Add our values + for n in 0..100 { + a.push(n); + b.push(n); + } + + // We have inserted the same items into both filters so we'd expect the + // symmetric difference to be zero if all is well. + let diff_size = a.size_with_sketch(&b); + + // But all is not well. This assertion fails! + assert_eq!(diff_size, 0.0); +} +``` + +The actual value returned in this example is ~200. This makes sense because the geofilter +thinks that there are 100 unique values in each of the filters, so the difference is approximated +as being ~200. If we were to rerun the above example with a genuinely reproducible `BuildHasher` +then the resulting diff size would be `0`. + +In debug builds, as part of the config's `eq` implementation, our library will assert that the `BuildHasher`s +produce the same `u64` value when given the same input but this is not enabled in release builds. + +## Stability + +Following from this, it might be important that your hash functions and seed values are stable, meaning, +that they won't change from one release to another. + +The default function provided in this library is *NOT* stable as it is based on the Rust standard libraries +`DefaultHasher` which does not have a specified algorithm and may change across releases of Rust. + +Stability is especially important to consider if you are using serialized geofilters which may have +been created in a previous version of the Rust standard library. + +This library provides an implementation of `ReproducibleBuildHasher` for the `FnvBuildHasher` provided +by the `fnv` crate version `1.0`. This is a _stable_ hash function in that it won't change unexpectedly +but it doesn't have good diffusion properties. This means if your input items have low entropy (for +example numbers from `0..10000`) you will find that the geofilter is not able to produce accurate estimations. + +## Uniformity and Diffusion + +In order to produce accurate estimations it is important that your hash function is able to produce evenly +distributed outputs for your input items. + +This property must be balanced against the performance requirements of your system as stronger hashing +algorithms are often slower. + +Depending on your input data, different functions may be more or less appropriate. For example, if your input +items have high entropy (e.g. SHA256 values) then the diffusion of your hash function might matter less. + +## Implementing your own `ReproducibleBuildHasher` type + +If you are using a hash function that you have not implemented yourself you will not be able to implement +`ReproducibleBuildHasher` on that type directly due to Rust's orphan rules. The easiest way to get around this +is to create a newtype which proxies the underlying `BuildHasher`. + +In addition to `BuildHasher` `ReproducibleBuildHasher` needs `Default` and `Clone`, which is usually implemented +on `BuildHasher`s, so you can probably just `#[derive(...)]` those. If your `BuildHasher` doesn't have those +traits then you may need to implement them manually. + +Here is an example of how to use new types to mark your `BuildHasher` as reproducible. + +```rust +#[derive(Clone, Default)] +pub struct MyBuildHasher(BuildHasherDefault); + +impl BuildHasher for MyBuildHasher { + type Hasher = DefaultHasher; + + fn build_hasher(&self) -> Self::Hasher { + self.0.build_hasher() + } +} + +impl ReproducibleBuildHasher for MyBuildHasher {} +``` diff --git a/crates/geo_filters/evaluation/accuracy.rs b/crates/geo_filters/evaluation/accuracy.rs index c3151b7..6248f07 100644 --- a/crates/geo_filters/evaluation/accuracy.rs +++ b/crates/geo_filters/evaluation/accuracy.rs @@ -2,6 +2,7 @@ use std::fs::File; use std::path::PathBuf; use clap::Parser; +use geo_filters::build_hasher::UnstableDefaultBuildHasher; use geo_filters::config::VariableConfig; use itertools::Itertools; use once_cell::sync::Lazy; @@ -156,19 +157,22 @@ static SIMULATION_CONFIG_FROM_STR: Lazy> = Lazy::new let [b, bytes, msb] = capture_usizes(&c, [2, 3, 4]); match t { BucketType::U8 => { - let c = VariableConfig::<_, u8>::new(b, bytes, msb); + let c = VariableConfig::<_, u8, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDiffCount::new(c.clone()))) } BucketType::U16 => { - let c = VariableConfig::<_, u16>::new(b, bytes, msb); + let c = + VariableConfig::<_, u16, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDiffCount::new(c.clone()))) } BucketType::U32 => { - let c = VariableConfig::<_, u32>::new(b, bytes, msb); + let c = + VariableConfig::<_, u32, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDiffCount::new(c.clone()))) } BucketType::U64 => { - let c = VariableConfig::<_, u64>::new(b, bytes, msb); + let c = + VariableConfig::<_, u64, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDiffCount::new(c.clone()))) } } @@ -185,19 +189,22 @@ static SIMULATION_CONFIG_FROM_STR: Lazy> = Lazy::new match t { BucketType::U8 => { - let c = VariableConfig::<_, u8>::new(b, bytes, msb); + let c = VariableConfig::<_, u8, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDistinctCount::new(c.clone()))) } BucketType::U16 => { - let c = VariableConfig::<_, u16>::new(b, bytes, msb); + let c = + VariableConfig::<_, u16, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDistinctCount::new(c.clone()))) } BucketType::U32 => { - let c = VariableConfig::<_, u32>::new(b, bytes, msb); + let c = + VariableConfig::<_, u32, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDistinctCount::new(c.clone()))) } BucketType::U64 => { - let c = VariableConfig::<_, u64>::new(b, bytes, msb); + let c = + VariableConfig::<_, u64, UnstableDefaultBuildHasher>::new(b, bytes, msb); Box::new(move || Box::new(GeoDistinctCount::new(c.clone()))) } } diff --git a/crates/geo_filters/evaluation/performance.rs b/crates/geo_filters/evaluation/performance.rs index 4d7d706..77a0ebd 100644 --- a/crates/geo_filters/evaluation/performance.rs +++ b/crates/geo_filters/evaluation/performance.rs @@ -1,4 +1,7 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_filters::build_hasher::UnstableDefaultBuildHasher; use geo_filters::config::VariableConfig; use geo_filters::diff_count::{GeoDiffCount, GeoDiffCount13}; use geo_filters::distinct_count::GeoDistinctCount13; @@ -20,7 +23,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }); group.bench_function("geo_diff_count_var_13", |b| { - let c = VariableConfig::<_, u32>::new(13, 7680, 256); + let c = VariableConfig::<_, u32, UnstableDefaultBuildHasher>::new(13, 7680, 256); b.iter(move || { let mut gc = GeoDiffCount::new(c.clone()); for i in 0..*size { @@ -59,7 +62,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }); group.bench_function("geo_diff_count_var_13", |b| { - let c = VariableConfig::<_, u32>::new(13, 7680, 256); + let c = VariableConfig::<_, u32, UnstableDefaultBuildHasher>::new(13, 7680, 256); b.iter(move || { let mut gc = GeoDiffCount::new(c.clone()); for i in 0..*size { @@ -104,7 +107,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }); group.bench_function("geo_diff_count_var_13", |b| { - let c = VariableConfig::<_, u32>::new(13, 7680, 256); + let c = VariableConfig::<_, u32, UnstableDefaultBuildHasher>::new(13, 7680, 256); b.iter(move || { let mut gc1 = GeoDiffCount::new(c.clone()); let mut gc2 = GeoDiffCount::new(c.clone()); diff --git a/crates/geo_filters/src/build_hasher.rs b/crates/geo_filters/src/build_hasher.rs new file mode 100644 index 0000000..0509bfd --- /dev/null +++ b/crates/geo_filters/src/build_hasher.rs @@ -0,0 +1,38 @@ +use std::hash::{BuildHasher, BuildHasherDefault, DefaultHasher, Hasher as _}; + +use fnv::FnvBuildHasher; + +/// Trait for a hasher factory that can be used to produce hashers +/// for use with geometric filters. +/// +/// It is a super set of [`BuildHasher`], enforcing additional requirements +/// on the hasher builder that are required for the geometric filters and +/// surrounding code. +/// +/// When performing operations across two different geometric filters, +/// the hashers must be equal, i.e. they must produce the same hash for the +/// same input. +pub trait ReproducibleBuildHasher: BuildHasher + Default + Clone { + #[inline] + fn debug_assert_hashers_eq() { + // In debug builds we check that hash outputs are the same for + // self and other. The library user should only have implemented + // our build hasher trait if this is already true, but we check + // here in case they have implemented the trait in error. + debug_assert_eq!( + Self::default().build_hasher().finish(), + Self::default().build_hasher().finish(), + "Hashers produced by ReproducibleBuildHasher do not produce the same output with the same input" + ); + } +} + +/// Note that this `BuildHasher` has a consistent implementation of `Default` +/// but is NOT stable across releases of Rust. It is therefore dangerous +/// to use if you plan on serializing the geofilters and reusing them due +/// to the fact that you can serialize a filter made with one version and +/// deserialize with another version of the hasher factor. +pub type UnstableDefaultBuildHasher = BuildHasherDefault; + +impl ReproducibleBuildHasher for UnstableDefaultBuildHasher {} +impl ReproducibleBuildHasher for FnvBuildHasher {} diff --git a/crates/geo_filters/src/config.rs b/crates/geo_filters/src/config.rs index b0e63bf..c830422 100644 --- a/crates/geo_filters/src/config.rs +++ b/crates/geo_filters/src/config.rs @@ -2,7 +2,7 @@ use std::{marker::PhantomData, sync::Arc}; -use crate::Method; +use crate::{build_hasher::ReproducibleBuildHasher, Method}; mod bitchunks; mod buckets; @@ -30,8 +30,9 @@ use once_cell::sync::Lazy; /// Those conversions can be shared across multiple geo filter instances. This way, the /// conversions can also be optimized via e.g. lookup tables without paying the cost with every /// new geo filter instance again and again. -pub trait GeoConfig: Clone + Eq + Sized + Send + Sync { +pub trait GeoConfig: Clone + Eq + Sized { type BucketType: IsBucketType + 'static; + type BuildHasher: ReproducibleBuildHasher; /// The number of most-significant bits that are stored sparsely as positions. fn max_msb_len(&self) -> usize; @@ -79,9 +80,16 @@ pub trait GeoConfig: Clone + Eq + Sized + Send + Sync { /// Instantiating this type may panic if `T` is too small to hold the maximum possible /// bucket id determined by `B`, or `B` is larger than the largest statically defined /// lookup table. -#[derive(Clone, Eq, PartialEq)] -pub struct FixedConfig { - _phantom: PhantomData<(M, T)>, +#[derive(Clone)] +pub struct FixedConfig< + M: Method, + T, + const B: usize, + const BYTES: usize, + const MSB: usize, + H: ReproducibleBuildHasher, +> { + _phantom: PhantomData<(M, T, H)>, } impl< @@ -90,9 +98,11 @@ impl< const B: usize, const BYTES: usize, const MSB: usize, - > GeoConfig for FixedConfig + H: ReproducibleBuildHasher, + > GeoConfig for FixedConfig { type BucketType = T; + type BuildHasher = H; #[inline] fn max_msb_len(&self) -> usize { @@ -148,42 +158,76 @@ impl< const B: usize, const BYTES: usize, const MSB: usize, - > Default for FixedConfig + H: ReproducibleBuildHasher, + > Default for FixedConfig { fn default() -> Self { assert_bucket_type_large_enough::(B); assert_buckets_within_estimation_bound(B, BYTES * BITS_PER_BYTE); + assert!( B < M::get_lookups().len(), "B = {} is not available for fixed config, requires B < {}", B, M::get_lookups().len() ); + Self { _phantom: PhantomData, } } } +impl< + M: Method + Lookups, + T: IsBucketType, + const B: usize, + const BYTES: usize, + const MSB: usize, + H: ReproducibleBuildHasher, + > PartialEq for FixedConfig +{ + fn eq(&self, _other: &Self) -> bool { + H::debug_assert_hashers_eq(); + + // The values of the fixed config are provided at compile time + // so no runtime computation is required + true + } +} + +impl< + M: Method + Lookups, + T: IsBucketType, + const B: usize, + const BYTES: usize, + const MSB: usize, + H: ReproducibleBuildHasher, + > Eq for FixedConfig +{ +} + /// Geometric filter configuration using dynamic lookup tables. #[derive(Clone)] -pub struct VariableConfig { +pub struct VariableConfig { b: usize, bytes: usize, msb: usize, - _phantom: PhantomData<(M, T)>, + _phantom: PhantomData<(M, T, H)>, lookup: Arc, } -impl Eq for VariableConfig {} +impl Eq for VariableConfig {} -impl PartialEq for VariableConfig { +impl PartialEq for VariableConfig { fn eq(&self, other: &Self) -> bool { + H::debug_assert_hashers_eq(); + self.b == other.b && self.bytes == other.bytes && self.msb == other.msb } } -impl VariableConfig { +impl VariableConfig { /// Returns a new configuration value. See [`FixedConfig`] for the meaning /// of the parameters. This functions computes a new lookup table every time /// it is invoked, so make sure to share the resulting value as much as possible. @@ -205,8 +249,11 @@ impl VariableConfig { } } -impl GeoConfig for VariableConfig { +impl GeoConfig + for VariableConfig +{ type BucketType = T; + type BuildHasher = H; #[inline] fn max_msb_len(&self) -> usize { @@ -306,13 +353,16 @@ pub(crate) fn take_ref(iter: &mut I, n: usize) -> impl Iterator>(f: impl Fn() -> C) -> (f32, f32) { - let mut rnd = rand::rngs::StdRng::from_entropy(); + pub(crate) fn test_estimate>( + rnd: &mut ChaCha12Rng, + f: impl Fn() -> C, + ) -> (f32, f32) { let cnt = 10000usize; let mut avg_precision = 0.0; let mut avg_var = 0.0; @@ -324,7 +374,7 @@ pub(crate) mod tests { m.push_hash(rnd.next_u64()); } // Compute the relative error between estimate and actually inserted items. - let high_precision = m.size() / cnt as f32 - 1.0; + let high_precision = m.size_f32() / cnt as f32 - 1.0; // Take the average over trials many attempts. avg_precision += high_precision / trials as f32; avg_var += high_precision.powf(2.0) / trials as f32; diff --git a/crates/geo_filters/src/config/buckets.rs b/crates/geo_filters/src/config/buckets.rs index 0d673c0..55cac2b 100644 --- a/crates/geo_filters/src/config/buckets.rs +++ b/crates/geo_filters/src/config/buckets.rs @@ -21,8 +21,7 @@ where fn into_block(self) -> u64 { assert!( self.into_usize() < BITS_PER_BLOCK, - "position in block must be less then 64, got {:?}", - self + "position in block must be less then 64, got {self:?}", ); 1u64 << self.into_usize() } diff --git a/crates/geo_filters/src/config/lookup.rs b/crates/geo_filters/src/config/lookup.rs index 637e7ac..3f7d688 100644 --- a/crates/geo_filters/src/config/lookup.rs +++ b/crates/geo_filters/src/config/lookup.rs @@ -45,29 +45,37 @@ impl HashToBucketLookup { #[cfg(test)] mod tests { - use rand::{RngCore, SeedableRng}; + use rand::RngCore; + use rand_chacha::ChaCha12Rng; - use crate::config::{hash_to_bucket, phi_f64}; + use crate::{ + config::{hash_to_bucket, phi_f64}, + test_rng::prng_test_harness, + }; use super::HashToBucketLookup; #[test] fn test_lookup_7() { - let var = lookup_random_hashes_variance::<7>(1 << 16); - assert!(var < 1e-4, "variance {} too large", var); + prng_test_harness(1, |rnd| { + let var = lookup_random_hashes_variance::<7>(rnd, 1 << 16); + assert!(var < 1e-4, "variance {var} too large"); + }); } #[test] fn test_lookup_13() { - let var = lookup_random_hashes_variance::<13>(1 << 16); - assert!(var < 1e-4, "variance {} too large", var); + prng_test_harness(1, |rnd| { + let var = lookup_random_hashes_variance::<13>(rnd, 1 << 16); + assert!(var < 1e-4, "variance {var} too large"); + }); } - fn lookup_random_hashes_variance(n: u64) -> f64 { + fn lookup_random_hashes_variance(rnd: &mut ChaCha12Rng, n: u64) -> f64 { let phi = phi_f64(B); let buckets = HashToBucketLookup::new(B); + let mut var = 0.0; - let mut rnd = rand::rngs::StdRng::from_entropy(); for _ in 0..n { let hash = rnd.next_u64(); let estimate = buckets.lookup(hash) as f64; diff --git a/crates/geo_filters/src/diff_count.rs b/crates/geo_filters/src/diff_count.rs index 2f1ccb2..6b2d04c 100644 --- a/crates/geo_filters/src/diff_count.rs +++ b/crates/geo_filters/src/diff_count.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::cmp::Ordering; +use std::hash::BuildHasher as _; use std::mem::{size_of, size_of_val}; +use std::ops::Deref as _; use crate::config::{ count_ones_from_bitchunks, count_ones_from_msb_and_lsb, iter_bit_chunks, iter_ones, @@ -16,6 +18,7 @@ mod sim_hash; use bitvec::*; pub use config::{GeoDiffConfig13, GeoDiffConfig7}; +pub use sim_hash::SimHash; /// Diff count filter with a relative error standard deviation of ~0.125. pub type GeoDiffCount7<'a> = GeoDiffCount<'a, GeoDiffConfig7>; @@ -76,7 +79,7 @@ impl> std::fmt::Debug for GeoDiffCount<'_, C> { } } -impl> GeoDiffCount<'_, C> { +impl<'a, C: GeoConfig> GeoDiffCount<'a, C> { pub fn new(config: C) -> Self { Self { config, @@ -94,21 +97,17 @@ impl> GeoDiffCount<'_, C> { /// having to construct another iterator with the remaining `BitChunk`s. fn from_bit_chunks>(config: C, chunks: I) -> Self { let mut ones = iter_ones::(chunks.peekable()); - let mut msb = Vec::default(); take_ref(&mut ones, config.max_msb_len() - 1).for_each(|bucket| { msb.push(bucket); }); let smallest_msb = ones .next() - .map(|bucket| { - msb.push(bucket); - bucket + .inspect(|bucket| { + msb.push(*bucket); }) .unwrap_or_default(); - let lsb = BitVec::from_bit_chunks(ones.into_bitchunks(), smallest_msb.into_usize()); - let result = Self { config, msb: Cow::from(msb), @@ -207,38 +206,47 @@ impl> GeoDiffCount<'_, C> { /// that makes the cost of the else case negligible. fn xor_bit(&mut self, bucket: C::BucketType) { if bucket.into_usize() < self.lsb.num_bits() { + // The bit being toggled is within our LSB bit vector + // so toggle it directly. self.lsb.toggle(bucket.into_usize()); } else { let msb = self.msb.to_mut(); match msb.binary_search_by(|k| bucket.cmp(k)) { Ok(idx) => { msb.remove(idx); - let (first, second) = { + let first = { let mut lsb = iter_ones(self.lsb.bit_chunks().peekable()); - (lsb.next(), lsb.next()) + lsb.next() }; - let new_smallest = if let Some(smallest) = first { + if let Some(smallest) = first { msb.push(C::BucketType::from_usize(smallest)); - second.map(|_| smallest).unwrap_or(0) + self.lsb.resize(smallest); } else { - 0 + self.lsb.resize(0); }; - self.lsb.resize(new_smallest); } Err(idx) => { msb.insert(idx, bucket); if msb.len() > self.config.max_msb_len() { + // We have too many values in the MSB sparse index vector, + // let's move the smalles MSB value into the LSB bit vector let smallest = msb .pop() .expect("we should have at least one element!") .into_usize(); - // ensure vector covers smallest let new_smallest = msb .last() .expect("should have at least one element") .into_usize(); + // ensure LSB bit vector has the space for `smallest` self.lsb.resize(new_smallest); self.lsb.toggle(smallest); + } else if msb.len() == self.config.max_msb_len() { + let smallest = msb + .last() + .expect("should have at least one element") + .into_usize(); + self.lsb.resize(smallest); } } } @@ -280,6 +288,104 @@ impl> GeoDiffCount<'_, C> { self.lsb.num_bits(), ); } + + // Serialization: + // + // Since most of our target platforms are little endian there are more optimised approaches + // for little endian platforms, just splatting the bytes into the writer. This is contrary + // to the usual "network endian" approach where big endian is the default, but most of our + // consumers are little endian so it makes sense for this to be the optimal approach. + // + // For now we do not support big endian platforms. In the future we might add a big endian + // platform specific implementation which is able to read the little endian serialized + // representation. For now, if you attempt to serialize a filter on a big endian platform + // you get a panic. + + /// Create a new [`GeoDiffCount`] from a slice of bytes + #[cfg(target_endian = "little")] + pub fn from_bytes_with_config(c: C, buf: &'a [u8]) -> Self { + if buf.is_empty() { + return Self::new(c); + } + // The number of most significant bits stores in the MSB sparse repr + let msb_len = (buf.len() / size_of::()).min(c.max_msb_len()); + let msb = unsafe { + std::mem::transmute::<&[u8], &[C::BucketType]>(std::slice::from_raw_parts( + buf.as_ptr(), + msb_len, + )) + }; + // The number of bytes representing the MSB - this is how many bytes we need to + // skip over to reach the LSB + let msb_bytes_len = msb_len * size_of::(); + Self { + config: c, + msb: Cow::Borrowed(msb), + lsb: BitVec::from_bytes(&buf[msb_bytes_len..]), + } + } + + #[cfg(target_endian = "little")] + pub fn write(&self, writer: &mut W) -> std::io::Result { + if self.msb.is_empty() { + return Ok(0); + } + let msb_buckets = self.msb.deref(); + let msb_bytes = unsafe { + std::slice::from_raw_parts(msb_buckets.as_ptr() as *const u8, size_of_val(msb_buckets)) + }; + writer.write_all(msb_bytes)?; + let mut bytes_written = msb_bytes.len(); + bytes_written += self.lsb.write(writer)?; + Ok(bytes_written) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn from_ones_with_config(config: C, ones: impl IntoIterator) -> Self { + let mut result = Self::new(config); + for one in ones { + result.xor_bit(one); + } + result + } + + #[cfg(any(test, feature = "test-support"))] + pub fn iter_ones(&self) -> impl Iterator + '_ { + iter_ones(self.bit_chunks().peekable()).map(C::BucketType::from_usize) + } + + /// Generate a pseudo-random filter. The RNG used to build the filter + /// is seeded using the number of items so for a given number of items + /// the resulting geofilter should always be the same. + #[cfg(any(test, feature = "test-support"))] + pub fn pseudorandom_filter_with_config(config: C, items: usize) -> Self { + use rand::RngCore; + use rand_chacha::rand_core::SeedableRng; + + let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(items as u64); + let mut filter = Self::new(config); + for _ in 0..items { + filter.push_hash(rng.next_u64()); + } + filter + } +} + +impl<'a, C: GeoConfig + Default> GeoDiffCount<'a, C> { + #[cfg(target_endian = "little")] + pub fn from_bytes(buf: &'a [u8]) -> Self { + Self::from_bytes_with_config(C::default(), buf) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn from_ones(ones: impl IntoIterator) -> Self { + Self::from_ones_with_config(C::default(), ones) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn pseudorandom_filter(items: usize) -> Self { + Self::pseudorandom_filter_with_config(C::default(), items) + } } /// Applies a repeated bit mask to the underlying filter. @@ -304,6 +410,10 @@ pub(crate) fn masked>( /// Computes an xor of the two underlying bitsets. /// This operation corresponds to computing the symmetric difference of the two /// sets represented by the GeoDiffCounts. +/// +/// # Panics +/// +/// Panics if the configuration of the geofilters is not identical. pub(crate) fn xor>( diff_count: &GeoDiffCount<'_, C>, other: &GeoDiffCount<'_, C>, @@ -312,6 +422,7 @@ pub(crate) fn xor>( diff_count.config == other.config, "combined filters must have the same configuration" ); + GeoDiffCount::from_bit_chunks( diff_count.config.clone(), xor_bit_chunks(diff_count.bit_chunks(), other.bit_chunks()).peekable(), @@ -323,15 +434,20 @@ impl> Count for GeoDiffCount<'_, C> { self.xor_bit(self.config.hash_to_bucket(hash)); } + fn push(&mut self, item: I) { + let build_hasher = C::BuildHasher::default(); + self.push_hash(build_hasher.hash_one(item)); + } + fn push_sketch(&mut self, other: &Self) { *self = xor(self, other); } - fn size(&self) -> f32 { + fn size_f32(&self) -> f32 { self.estimate_size() } - fn size_with_sketch(&self, other: &Self) -> f32 { + fn size_with_sketch_f32(&self, other: &Self) -> f32 { assert!( self.config == other.config, "combined filters must have the same configuration" @@ -341,16 +457,24 @@ impl> Count for GeoDiffCount<'_, C> { fn bytes_in_memory(&self) -> usize { let Self { config, msb, lsb } = self; + size_of_val(config) + msb.len() * size_of::() + lsb.bytes_in_memory() } } #[cfg(test)] mod tests { + use std::io::Write; + use itertools::Itertools; - use rand::{RngCore, SeedableRng}; + use rand::{seq::IteratorRandom, RngCore}; + use rand_chacha::ChaCha12Rng; - use crate::config::{iter_ones, tests::test_estimate, FixedConfig}; + use crate::{ + build_hasher::UnstableDefaultBuildHasher, + config::{tests::test_estimate, FixedConfig}, + test_rng::prng_test_harness, + }; use super::*; @@ -359,7 +483,8 @@ mod tests { // // scripts/accuracy -n 10000 geo_diff/u16/b=7/bytes=50/msb=10 // - type GeoDiffCount7_50<'a> = GeoDiffCount<'a, FixedConfig>; + type GeoDiffCount7_50<'a> = + GeoDiffCount<'a, FixedConfig>; #[test] fn test_geo_count() { @@ -374,15 +499,16 @@ mod tests { (10000000, 10194611.0), ] { let mut geo_count = GeoDiffCount13::default(); + (0..n).for_each(|i| geo_count.push(i)); - assert_eq!(result, geo_count.size()); + assert_eq!(result, geo_count.size_f32()); } } #[test] fn test_xor() { - let a = GeoDiffCount7::from_ones(Default::default(), 0..1000); - let b = GeoDiffCount7::from_ones(Default::default(), 10..1010); + let a = GeoDiffCount7::from_ones(0..1000); + let b = GeoDiffCount7::from_ones(10..1010); let c = xor(&a, &b); let d = xor(&a, &b); assert_eq!(a.iter_ones().count(), 1000); @@ -402,7 +528,7 @@ mod tests { m.xor_bit(10); assert!(m.iter_ones().collect_vec().is_empty()); - let mut m = GeoDiffCount7::from_ones(Default::default(), 0..100); + let mut m = GeoDiffCount7::from_ones(0..100); assert_eq!(m.iter_ones().count(), 100); m.xor_bit(10); assert_eq!(m.iter_ones().count(), 99); @@ -418,57 +544,62 @@ mod tests { #[test] fn test_estimate_fast() { - let (avg_precision, avg_var) = test_estimate(GeoDiffCount7::default); - println!( - "avg precision: {} with standard deviation: {}", - avg_precision, - avg_var.sqrt(), - ); - // Make sure that the estimate converges to the correct value. - assert!(avg_precision.abs() < 0.04); - // We should theoretically achieve a standard deviation of about 0.12 - assert!(avg_var.sqrt() < 0.14); + prng_test_harness(1, |rnd| { + let (avg_precision, avg_var) = test_estimate(rnd, GeoDiffCount7::default); + println!( + "avg precision: {} with standard deviation: {}", + avg_precision, + avg_var.sqrt(), + ); + // Make sure that the estimate converges to the correct value. + assert!(avg_precision.abs() < 0.04); + // We should theoretically achieve a standard deviation of about 0.12 + assert!(avg_var.sqrt() < 0.14); + }) } #[test] fn test_estimate_fast_low_precision() { - let (avg_precision, avg_var) = test_estimate(GeoDiffCount7_50::default); - println!( - "avg precision: {} with standard deviation: {}", - avg_precision, - avg_var.sqrt(), - ); - // Make sure that the estimate converges to the correct value. - assert!(avg_precision.abs() < 0.15); - // We should theoretically achieve a standard deviation of about 0.25 - assert!(avg_var.sqrt() < 0.4); + prng_test_harness(1, |rnd| { + let (avg_precision, avg_var) = test_estimate(rnd, GeoDiffCount7_50::default); + println!( + "avg precision: {} with standard deviation: {}", + avg_precision, + avg_var.sqrt(), + ); + // Make sure that the estimate converges to the correct value. + assert!(avg_precision.abs() < 0.15); + // We should theoretically achieve a standard deviation of about 0.25 + assert!(avg_var.sqrt() < 0.4); + }); } #[test] fn test_estimate_diff_size_fast() { - let mut rnd = rand::rngs::StdRng::from_entropy(); - let mut a_p = GeoDiffCount7_50::default(); - let mut a_hp = GeoDiffCount7::default(); - let mut b_p = GeoDiffCount7_50::default(); - let mut b_hp = GeoDiffCount7::default(); - for _ in 0..10000 { - let hash = rnd.next_u64(); - a_p.push_hash(hash); - a_hp.push_hash(hash); - } - for _ in 0..1000 { - let hash = rnd.next_u64(); - b_p.push_hash(hash); - b_hp.push_hash(hash); - } - let c_p = xor(&a_p, &b_p); - let c_hp = xor(&a_hp, &b_hp); + prng_test_harness(1, |rnd| { + let mut a_p = GeoDiffCount7_50::default(); + let mut a_hp = GeoDiffCount7::default(); + let mut b_p = GeoDiffCount7_50::default(); + let mut b_hp = GeoDiffCount7::default(); + for _ in 0..10000 { + let hash = rnd.next_u64(); + a_p.push_hash(hash); + a_hp.push_hash(hash); + } + for _ in 0..1000 { + let hash = rnd.next_u64(); + b_p.push_hash(hash); + b_hp.push_hash(hash); + } + let c_p = xor(&a_p, &b_p); + let c_hp = xor(&a_hp, &b_hp); - assert_eq!(c_p.size(), a_p.size_with_sketch(&b_p)); - assert_eq!(c_p.size(), b_p.size_with_sketch(&a_p)); + assert_eq!(c_p.size(), a_p.size_with_sketch(&b_p)); + assert_eq!(c_p.size(), b_p.size_with_sketch(&a_p)); - assert_eq!(c_hp.size(), a_hp.size_with_sketch(&b_hp)); - assert_eq!(c_hp.size(), b_hp.size_with_sketch(&a_hp)); + assert_eq!(c_hp.size(), a_hp.size_with_sketch(&b_hp)); + assert_eq!(c_hp.size(), b_hp.size_with_sketch(&a_hp)); + }); } #[test] @@ -479,20 +610,19 @@ mod tests { // masked bitset : 010000 100100 000000 // after compression : 01 0 10 1 00 0 // bitset of the returned filter : 010 101000 - let m = GeoDiffCount7::from_ones(Default::default(), [16, 15, 13, 11, 9, 8, 6, 3, 1]); + let m = GeoDiffCount7::from_ones([16, 15, 13, 11, 9, 8, 6, 3, 1]); let n = masked(&m, 0b110100, 6); assert_eq!(n.iter_ones().collect_vec(), vec![16, 11, 8]); for i in 0..100 { - let m = GeoDiffCount7::from_ones(Default::default(), (0..i).collect_vec()); + let m = GeoDiffCount7::from_ones((0..i).collect_vec()); let n = masked(&m, 0b111, 3); assert_eq!(m, n); } for i in 0..300 { - let m = GeoDiffCount7::from_ones(Default::default(), (0..i).collect_vec()); - let slow = - GeoDiffCount::from_ones(Default::default(), masked(&m, 0b110, 3).iter_ones()); + let m = GeoDiffCount7::from_ones((0..i).collect_vec()); + let slow = GeoDiffCount::from_ones(masked(&m, 0b110, 3).iter_ones()); let n = masked(&m, 0b110, 3); assert_eq!(slow, n, "in iteration: {i}"); } @@ -500,45 +630,39 @@ mod tests { #[test] fn test_xor_plus_mask() { - let mut rnd = rand::rngs::StdRng::from_entropy(); - let mask_size = 12; - let mask = 0b100001100000; - let mut a = GeoDiffCount7::default(); - for _ in 0..10000 { - a.xor_bit(a.config.hash_to_bucket(rnd.next_u64())); - } - let mut expected = GeoDiffCount7::default(); - let mut b = a.clone(); - for _ in 0..1000 { - let hash = rnd.next_u64(); - b.xor_bit(b.config.hash_to_bucket(hash)); - expected.xor_bit(expected.config.hash_to_bucket(hash)); - assert_eq!(expected, xor(&a, &b)); - - let masked_a = masked(&a, mask, mask_size); - let masked_b = masked(&b, mask, mask_size); - let masked_expected = masked(&expected, mask, mask_size); - // FIXME: test failed once with: - // left: ~12.37563 (msb: [390, 334, 263, 242, 222, 215, 164, 148, 100, 97, 66, 36], |lsb|: 36) - // right: ~12.37563 (msb: [390, 334, 263, 242, 222, 215, 164, 148, 100, 97, 66, 36], |lsb|: 0) - assert_eq!(masked_expected, xor(&masked_a, &masked_b)); - } + prng_test_harness(10, |rnd| { + let mask_size = 12; + let mask = 0b100001100000; + let mut a = GeoDiffCount7::default(); + for _ in 0..10000 { + a.xor_bit(a.config.hash_to_bucket(rnd.next_u64())); + } + let mut expected = GeoDiffCount7::default(); + let mut b = a.clone(); + for _ in 0..1000 { + let hash = rnd.next_u64(); + b.xor_bit(b.config.hash_to_bucket(hash)); + expected.xor_bit(expected.config.hash_to_bucket(hash)); + assert_eq!(expected, xor(&a, &b)); + let masked_a = masked(&a, mask, mask_size); + let masked_b = masked(&b, mask, mask_size); + let masked_expected = masked(&expected, mask, mask_size); + assert_eq!(masked_expected, xor(&masked_a, &masked_b)); + } + }); } #[test] fn test_bit_chunks() { - let mut rnd = rand::rngs::StdRng::from_entropy(); - for _ in 0..100 { + prng_test_harness(100, |rnd| { let mut expected = GeoDiffCount7::default(); for _ in 0..1000 { expected.push_hash(rnd.next_u64()); } - let actual = GeoDiffCount::from_bit_chunks( - expected.config.clone(), - expected.bit_chunks().peekable(), - ); + let actual = + GeoDiffCount::from_bit_chunks(expected.config.clone(), expected.bit_chunks()); assert_eq!(expected, actual); - } + }); } #[test] @@ -550,17 +674,64 @@ mod tests { assert_eq!(vec![17, 11, 7], a.msb.iter().copied().collect_vec()); } - impl> GeoDiffCount<'_, C> { - fn from_ones(config: C, ones: impl IntoIterator) -> Self { - let mut result = Self::new(config); - for one in ones { - result.xor_bit(one); - } - result - } + #[test] + fn test_serialization_empty() { + let before = GeoDiffCount7::default(); + + let mut writer = vec![]; + before.write(&mut writer).unwrap(); + + assert_eq!(writer.len(), 0); + + let after = GeoDiffCount7::from_bytes_with_config(before.config.clone(), &writer); + + assert_eq!(before, after); + } - fn iter_ones(&self) -> impl Iterator + '_ { - iter_ones(self.bit_chunks().peekable()).map(C::BucketType::from_usize) + // This helper exists in order to easily test serializing types with different + // bucket types in the MSB sparse bit field representation. See tests below. + #[cfg(target_endian = "little")] + fn serialization_round_trip + Default>(rnd: &mut ChaCha12Rng) { + // Run 100 simulations of random values being put into + // a diff counter. "Serializing" to a vector to emulate + // writing to a disk, and then deserializing and asserting + // the filters are equal. + let mut before = GeoDiffCount::<'_, C>::default(); + // Select a random number of items to insert. + let items = (1..1000).choose(rnd).unwrap(); + for _ in 0..items { + before.push_hash(rnd.next_u64()); } + let mut writer = vec![]; + // Insert some padding to emulate alignment issues with the slices. + // A previous version of this test never panicked even though we were + // violating the alignment preconditions for the `from_raw_parts` function. + let padding = [0_u8; 8]; + let pad_amount = (0..8).choose(rnd).unwrap(); + writer.write_all(&padding[..pad_amount]).unwrap(); + before.write(&mut writer).unwrap(); + let after = GeoDiffCount::<'_, C>::from_bytes_with_config( + before.config.clone(), + &writer[pad_amount..], + ); + assert_eq!(before, after); + } + + #[test] + #[cfg(target_endian = "little")] + fn test_serialization_round_trip_7() { + prng_test_harness(100, |rnd| { + // Uses a u16 for MSB buckets. + serialization_round_trip::(rnd); + }); + } + + #[test] + #[cfg(target_endian = "little")] + fn test_serialization_round_trip_13() { + prng_test_harness(100, |rnd| { + // Uses a u32 for MSB buckets. + serialization_round_trip::(rnd); + }); } } diff --git a/crates/geo_filters/src/diff_count/bitvec.rs b/crates/geo_filters/src/diff_count/bitvec.rs index c94a041..f77323c 100644 --- a/crates/geo_filters/src/diff_count/bitvec.rs +++ b/crates/geo_filters/src/diff_count/bitvec.rs @@ -1,18 +1,17 @@ use std::borrow::Cow; use std::cmp::Ordering; -use std::iter::Peekable; use std::mem::{size_of, size_of_val}; -use std::ops::{Index, Range}; +use std::ops::{Deref as _, Index, Range}; -use crate::config::BitChunk; use crate::config::IsBucketType; use crate::config::BITS_PER_BLOCK; +use crate::config::{BitChunk, BYTES_PER_BLOCK}; /// A bit vector where every bit occupies exactly one bit (in contrast to `Vec` where each /// bit consumes 1 byte). It only implements the minimum number of operations that we need for our /// GeoDiffCount implementation. In particular it supports xor-ing of two bit vectors and /// iterating through one bits. -#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Default, Debug, Eq, PartialEq)] pub(crate) struct BitVec<'a> { num_bits: usize, blocks: Cow<'a, [u64]>, @@ -37,15 +36,7 @@ impl BitVec<'_> { /// Takes an iterator of `BitChunk` items as input and returns the corresponding `BitVec`. /// The order of `BitChunk`s doesn't matter for this function and `BitChunk` may be hitting /// the same block. In this case, the function will simply xor them together. - /// - /// NOTE: If the bitchunks iterator is empty, the result is NOT sized to `num_bits` but will - /// be EMPTY instead. - pub fn from_bit_chunks>( - mut chunks: Peekable, - num_bits: usize, - ) -> Self { - // if there are no chunks, we keep the size zero - let num_bits = chunks.peek().map(|_| num_bits).unwrap_or_default(); + pub fn from_bit_chunks>(chunks: I, num_bits: usize) -> Self { let mut result = Self::default(); result.resize(num_bits); let blocks = result.blocks.to_mut(); @@ -81,6 +72,10 @@ impl BitVec<'_> { self.num_bits } + pub fn is_empty(&self) -> bool { + self.num_bits() == 0 + } + /// Tests the bit specified by the provided zero-based bit position. pub fn test_bit(&self, index: usize) -> bool { assert!(index < self.num_bits); @@ -142,6 +137,53 @@ impl BitVec<'_> { let Self { num_bits, blocks } = self; size_of_val(num_bits) + blocks.len() * size_of::() } + + #[cfg(target_endian = "little")] + pub fn from_bytes(mut buf: &[u8]) -> Self { + if buf.is_empty() { + return Self::default(); + } + // The first byte of the serialized BitVec is used to indicate how many + // of the bits in the left-most u64 block are *unoccupied*. + // See [`BitVec::write`] implementation for how this is done. + assert!( + buf[0] < 64, + "Number of unoccupied bits should be <64, got {}", + buf[0] + ); + let num_bits = (buf.len() - 1) * 8 - buf[0] as usize; + buf = &buf[1..]; + assert_eq!( + buf.len() % BYTES_PER_BLOCK, + 0, + "buffer should be a multiple of 8 bytes, got {}", + buf.len() + ); + let blocks = unsafe { + std::mem::transmute::<&[u8], &[u64]>(std::slice::from_raw_parts( + buf.as_ptr(), + buf.len() / BYTES_PER_BLOCK, + )) + }; + let blocks = Cow::Borrowed(blocks); + Self { num_bits, blocks } + } + + #[cfg(target_endian = "little")] + pub fn write(&self, writer: &mut W) -> std::io::Result { + if self.is_empty() { + return Ok(0); + } + // First serialize the number of unoccupied bits in the last block as one byte. + let unoccupied_bits = 63 - ((self.num_bits - 1) % 64) as u8; + writer.write_all(&[unoccupied_bits])?; + let blocks = self.blocks.deref(); + let block_bytes = unsafe { + std::slice::from_raw_parts(blocks.as_ptr() as *const u8, blocks.len() * BYTES_PER_BLOCK) + }; + writer.write_all(block_bytes)?; + Ok(block_bytes.len() + 1) + } } impl Index for BitVec<'_> { diff --git a/crates/geo_filters/src/diff_count/config.rs b/crates/geo_filters/src/diff_count/config.rs index 7ad7447..365c04d 100644 --- a/crates/geo_filters/src/diff_count/config.rs +++ b/crates/geo_filters/src/diff_count/config.rs @@ -1,5 +1,6 @@ use once_cell::sync::Lazy; +use crate::build_hasher::UnstableDefaultBuildHasher; use crate::config::EstimationLookup; use crate::config::FixedConfig; use crate::config::HashToBucketLookup; @@ -17,7 +18,7 @@ use crate::Diff; // // scripts/accuracy -n 10000 geo_diff/u16/b=7/bytes=112/msb={8,12,16,20} // -pub type GeoDiffConfig7 = FixedConfig; +pub type GeoDiffConfig7 = FixedConfig; /// Diff count configuration with a relative error standard deviation of ~0.015. // @@ -29,7 +30,7 @@ pub type GeoDiffConfig7 = FixedConfig; // // scripts/accuracy -n 1000 geo_diff/u32/b=13/bytes=7138/msb={128,192,256,384,512} // -pub type GeoDiffConfig13 = FixedConfig; +pub type GeoDiffConfig13 = FixedConfig; impl Lookups for Diff { #[inline] @@ -124,7 +125,7 @@ mod tests { #[test] fn test_bit_from_hash() { - let config = GeoDiffConfig7::default(); + let config = GeoDiffConfig7::::default(); assert_eq!(config.hash_to_bucket(u64::MAX), 0); assert_eq!( config.hash_to_bucket(0) as usize, @@ -169,7 +170,7 @@ mod tests { #[test] fn test_estimation_lut_7() { - let c = GeoDiffConfig7::default(); + let c = GeoDiffConfig7::::default(); let err = (0..600) .step_by(1) .map(|i| { @@ -188,7 +189,7 @@ mod tests { #[test] fn test_estimation_lut_13() { - let c = GeoDiffConfig13::default(); + let c = GeoDiffConfig13::::default(); let err = (0..24000) .step_by(100) .map(|i| { diff --git a/crates/geo_filters/src/diff_count/sim_hash.rs b/crates/geo_filters/src/diff_count/sim_hash.rs index cee2370..5c92aa6 100644 --- a/crates/geo_filters/src/diff_count/sim_hash.rs +++ b/crates/geo_filters/src/diff_count/sim_hash.rs @@ -11,16 +11,22 @@ use crate::Diff; use super::BitVec; +// TODO migrate these const values to be defined in configuration +// The current values are only really appropriate for the smaller +// diff configuration. + /// Number of bits covered by each SimHash bucket. -pub(crate) const SIM_BUCKET_SIZE: usize = 6; +const SIM_BUCKET_SIZE: usize = 6; /// Number of consecutive SimHash buckets used for searching. -pub(crate) const SIM_BUCKETS: usize = 20; +const SIM_BUCKETS: usize = 20; pub type BucketId = usize; /// SimHash is a hash computed over a continuous range of bits from a GeoDiffCount. /// It is used to quickly find similar sets with a reverse index. #[derive(Copy, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct SimHash(pub u64); impl SimHash { @@ -71,7 +77,7 @@ impl> GeoDiffCount<'_, C> { /// The first argument in the tuple is the bucket id of the `SimHash` which can be used /// to select a certain subset of `SimHashes`. SimHashes are returned in decreasing order /// of bucket ids, since that's their natural construction order. - pub fn sim_hashes(&self) -> impl Iterator + '_ { + pub fn sim_hashes(&self) -> impl ExactSizeIterator + '_ { SimHashIterator::new(self) } @@ -83,15 +89,29 @@ impl> GeoDiffCount<'_, C> { .map(|(_, sim_hash)| sim_hash) } + /// Get the `SimHash`es for this filter for the purpose of performing a search. + /// + /// Returns an iterator of the `SimHash`es and a number representing the minimum number + /// of matches required to consider this filter a match to a given filter, given + /// the expected diff size. + /// + /// The geo_filter can be used to do an "exact" search by setting expected_diff_size to zero. + /// In this case, all the buckets must match. Similarly, small differences can be found by + /// requiring (SIM_BUCKETS - expected_diff_size) many buckets to match. For larger differences + /// SIM_BUCKETS / 2 many buckets have to match. pub fn sim_hashes_search( &self, expected_diff_size: usize, - ) -> impl Iterator + '_ { + ) -> (impl Iterator + '_, usize) { let range = self.sim_hash_range(expected_diff_size); - self.sim_hashes() + let sim_hash_iter = self.sim_hashes(); + let n = range.len().min(sim_hash_iter.len()); + let min_matches = n.saturating_sub(expected_diff_size).max(SIM_BUCKETS / 2); + let filtered_iter = sim_hash_iter .skip_while(move |(bucket_id, _)| *bucket_id >= range.end) .take_while(move |(bucket_id, _)| *bucket_id >= range.start) - .map(|(_, sim_hash)| sim_hash) + .map(|(_, sim_hash)| sim_hash); + (filtered_iter, min_matches) } } @@ -146,8 +166,14 @@ impl> Iterator for SimHashIterator<'_, C> { SimHash::new(self.prev_bucket_id, self.sim_hash[bucket]), )) } + + fn size_hint(&self) -> (usize, Option) { + (self.prev_bucket_id, Some(self.prev_bucket_id)) + } } +impl> ExactSizeIterator for SimHashIterator<'_, C> {} + impl> GeoDiffCount<'_, C> { /// n specifies the desired zero-based index of the most significant one. /// The zero-based index of the desired one bit is returned. diff --git a/crates/geo_filters/src/distinct_count.rs b/crates/geo_filters/src/distinct_count.rs index ccd8bc8..dd90759 100644 --- a/crates/geo_filters/src/distinct_count.rs +++ b/crates/geo_filters/src/distinct_count.rs @@ -1,6 +1,7 @@ //! Geometric filter implementation for distinct count. use std::collections::VecDeque; +use std::hash::BuildHasher as _; use std::mem::{size_of, size_of_val}; use crate::config::{ @@ -133,11 +134,16 @@ impl> Count for GeoDistinctCount<'_, C> { self.set_bit(self.config.hash_to_bucket(hash)); } + fn push(&mut self, item: I) { + let build_hasher = C::BuildHasher::default(); + self.push_hash(build_hasher.hash_one(item)); + } + fn push_sketch(&mut self, other: &Self) { *self = or(self, other) } - fn size(&self) -> f32 { + fn size_f32(&self) -> f32 { let lowest_bucket = self.lsb.bit_range().start; let total = self.msb.len() + self @@ -153,7 +159,7 @@ impl> Count for GeoDistinctCount<'_, C> { } } - fn size_with_sketch(&self, other: &Self) -> f32 { + fn size_with_sketch_f32(&self, other: &Self) -> f32 { assert!( self.config == other.config, "combined filters must have the same configuration" @@ -216,25 +222,29 @@ fn or>( a.config == b.config, "combined filters must have the same configuration" ); - GeoDistinctCount::from_bit_chunks( + + GeoDistinctCount::<'static, C>::from_bit_chunks( a.config.clone(), - or_bit_chunks(a.bit_chunks(), b.bit_chunks()).peekable(), + or_bit_chunks(a.bit_chunks(), b.bit_chunks()), ) } #[cfg(test)] mod tests { use itertools::Itertools; - use rand::{RngCore, SeedableRng}; + use rand::RngCore; + use crate::build_hasher::UnstableDefaultBuildHasher; use crate::config::{iter_ones, tests::test_estimate, FixedConfig, VariableConfig}; use crate::evaluation::simulation::simulate; + use crate::test_rng::prng_test_harness; use super::*; #[test] fn test_lookup_table() { - let c = FixedConfig::::default(); + let c = + FixedConfig::::default(); for i in 0..c.max_bytes() * 4 { let hash = (c.phi_f64().powf(i as f64 + 0.5) * u64::MAX as f64).round() as u64; let a = c.hash_to_bucket(hash); @@ -245,6 +255,11 @@ mod tests { #[test] fn test_geo_count() { + // Pairs of (n, expected) where n is the number of inserted items + // and expected is the expected size of the GeoDistinctCount. + // The output matching the expected values is dependent on the configuration + // and hashing function. Changes to these will lead to different results and the + // test will need to be updated. for (n, result) in [ (10, 10.0021105), (100, 100.21153), @@ -257,7 +272,7 @@ mod tests { ] { let mut geo_count = GeoDistinctCount13::default(); (0..n).for_each(|i| geo_count.push(i)); - assert_eq!(result, geo_count.size()); + assert_eq!(result, geo_count.size_f32()); } } @@ -293,33 +308,36 @@ mod tests { #[test] fn test_estimate_fast() { - let (avg_precision, avg_var) = test_estimate(GeoDistinctCount7::default); - println!( - "avg precision: {} with standard deviation: {}", - avg_precision, - avg_var.sqrt(), - ); - // Make sure that the estimate converges to the correct value. - assert!(avg_precision.abs() < 0.04); - // We should theoretically achieve a standard deviation of about 0.065 - assert!(avg_var.sqrt() < 0.08); + prng_test_harness(1, |rnd| { + let (avg_precision, avg_var) = test_estimate(rnd, GeoDistinctCount7::default); + println!( + "avg precision: {} with standard deviation: {}", + avg_precision, + avg_var.sqrt(), + ); + // Make sure that the estimate converges to the correct value. + assert!(avg_precision.abs() < 0.04); + // We should theoretically achieve a standard deviation of about 0.065 + assert!(avg_var.sqrt() < 0.08); + }) } #[test] fn test_estimate_union_size_fast() { - let mut rnd = rand::rngs::StdRng::from_entropy(); - let mut a = GeoDistinctCount7::default(); - let mut b = GeoDistinctCount7::default(); - for _ in 0..10000 { - a.push_hash(rnd.next_u64()); - } - for _ in 0..1000 { - b.push_hash(rnd.next_u64()); - } - let c = or(&a, &b); + prng_test_harness(1, |rnd| { + let mut a = GeoDistinctCount7::default(); + let mut b = GeoDistinctCount7::default(); + for _ in 0..10000 { + a.push_hash(rnd.next_u64()); + } + for _ in 0..1000 { + b.push_hash(rnd.next_u64()); + } + let c = or(&a, &b); - assert_eq!(c.size(), a.size_with_sketch(&b)); - assert_eq!(c.size(), b.size_with_sketch(&a)); + assert_eq!(c.size(), a.size_with_sketch(&b)); + assert_eq!(c.size(), b.size_with_sketch(&a)); + }) } fn golden_section_min f32>(min: f32, max: f32, f: F) -> f32 { @@ -358,7 +376,11 @@ mod tests { let msb = golden_section_min(1.0, 1000.0, |msb| { simulate( || { - Box::new(GeoDistinctCount::new(VariableConfig::<_, u32>::new( + Box::new(GeoDistinctCount::new(VariableConfig::< + _, + u32, + UnstableDefaultBuildHasher, + >::new( 13, 7800, (7800 - (msb.round() as usize) * 8) / 3, @@ -374,8 +396,7 @@ mod tests { #[test] fn test_bit_chunks() { - let mut rnd = rand::rngs::StdRng::from_entropy(); - for _ in 0..100 { + prng_test_harness(100, |rnd| { let mut expected = GeoDistinctCount7::default(); for _ in 0..1000 { expected.push_hash(rnd.next_u64()); @@ -383,7 +404,7 @@ mod tests { let actual = GeoDistinctCount::from_bit_chunks(expected.config.clone(), expected.bit_chunks()); assert_eq!(expected, actual); - } + }) } #[test] diff --git a/crates/geo_filters/src/distinct_count/config.rs b/crates/geo_filters/src/distinct_count/config.rs index 6498fe5..1ac5163 100644 --- a/crates/geo_filters/src/distinct_count/config.rs +++ b/crates/geo_filters/src/distinct_count/config.rs @@ -1,5 +1,6 @@ use once_cell::sync::Lazy; +use crate::build_hasher::UnstableDefaultBuildHasher; use crate::config::EstimationLookup; use crate::config::FixedConfig; use crate::config::HashToBucketLookup; @@ -18,7 +19,8 @@ use crate::Distinct; // // scripts/accuracy -n 10000 geo_distinct/u16/b=7/bytes=136/msb={8,16,32,64} // -pub type GeoDistinctConfig7 = FixedConfig; +pub type GeoDistinctConfig7 = + FixedConfig; /// Distinct count configuration with a relative error standard deviation of ~0.0075. /// Uses at most 9248 bytes of memory. @@ -31,7 +33,8 @@ pub type GeoDistinctConfig7 = FixedConfig; // // scripts/accuracy -n 10000 geo_distinct/u32/b=13/bytes=9216/msb={128,192,256,320,512,640} // -pub type GeoDistinctConfig13 = FixedConfig; +pub type GeoDistinctConfig13 = + FixedConfig; impl Lookups for Distinct { #[inline] @@ -107,7 +110,7 @@ mod tests { #[test] fn test_estimation_lut_7() { - let c = GeoDistinctConfig7::default(); + let c = GeoDistinctConfig7::::default(); let err = (0..600) .step_by(1) .map(|i| { @@ -126,7 +129,7 @@ mod tests { #[test] fn test_estimation_lut_13() { - let c = GeoDistinctConfig13::default(); + let c = GeoDistinctConfig13::::default(); let err = (0..24000) .step_by(1) .map(|i| { diff --git a/crates/geo_filters/src/evaluation/hll.rs b/crates/geo_filters/src/evaluation/hll.rs index 30b74bf..2da68ac 100644 --- a/crates/geo_filters/src/evaluation/hll.rs +++ b/crates/geo_filters/src/evaluation/hll.rs @@ -8,7 +8,10 @@ use std::{ use hyperloglogplus::{HyperLogLog, HyperLogLogPlus}; -use crate::{Count, Distinct}; +use crate::{ + build_hasher::{ReproducibleBuildHasher, UnstableDefaultBuildHasher}, + Count, Distinct, +}; /// Uses at most 192 bytes. /// The relative error has a standard deviation of ~0.065. @@ -77,6 +80,7 @@ pub struct NoopHasher { hash: u64, } +#[derive(Clone, Default)] pub struct BuildNoopHasher {} impl Hasher for NoopHasher { @@ -87,7 +91,9 @@ impl Hasher for NoopHasher { #[inline] fn write(&mut self, _: &[u8]) { - todo!("") + unimplemented!( + "NoopHasher does not support arbitrary byte sequences. Use write_u64 instead" + ); } #[inline] @@ -104,21 +110,28 @@ impl BuildHasher for BuildNoopHasher { } } +impl ReproducibleBuildHasher for BuildNoopHasher {} + impl Count for Hll { fn push_hash(&mut self, hash: u64) { self.inner.borrow_mut().insert(&hash) } + fn push(&mut self, item: I) { + let build_hasher = UnstableDefaultBuildHasher::default(); + self.push_hash(build_hasher.hash_one(item)); + } + fn push_sketch(&mut self, _other: &Self) { - todo!() + unimplemented!() } - fn size(&self) -> f32 { + fn size_f32(&self) -> f32 { self.inner.borrow_mut().count() as f32 } - fn size_with_sketch(&self, _other: &Self) -> f32 { - todo!() + fn size_with_sketch_f32(&self, _other: &Self) -> f32 { + unimplemented!() } fn bytes_in_memory(&self) -> usize { diff --git a/crates/geo_filters/src/evaluation/simulation.rs b/crates/geo_filters/src/evaluation/simulation.rs index 9d3a131..b4de4ed 100644 --- a/crates/geo_filters/src/evaluation/simulation.rs +++ b/crates/geo_filters/src/evaluation/simulation.rs @@ -25,7 +25,7 @@ impl + Clone> SimulationCount for GeoDiffCount<'_, C> { >::push_hash(self, hash) } fn size(&self) -> f32 { - >::size(self) + >::size_f32(self) } fn bytes_in_memory(&self) -> usize { >::bytes_in_memory(self) @@ -36,7 +36,7 @@ impl> SimulationCount for GeoDistinctCount<'_, C> { >::push_hash(self, hash) } fn size(&self) -> f32 { - >::size(self) + >::size_f32(self) } fn bytes_in_memory(&self) -> usize { >::bytes_in_memory(self) @@ -47,7 +47,7 @@ impl SimulationCount for Hll { >::push_hash(self, hash) } fn size(&self) -> f32 { - >::size(self) + >::size_f32(self) } fn bytes_in_memory(&self) -> usize { >::bytes_in_memory(self) @@ -98,7 +98,7 @@ pub fn run_simulations( println!("Parameters:"); println!(); println!(" number of configs = {}", configs.len()); - println!(" number of samples = {}", samples); + println!(" number of samples = {samples}"); println!(" number of sets = {}", set_sizes.len()); println!(); @@ -107,7 +107,7 @@ pub fn run_simulations( let results = configs .iter() .map(|(name, f)| { - print!(" {} ... ", name); + print!(" {name} ... "); std::io::stdout().flush().expect("stdout can be flushed"); let t = Instant::now(); let result = simulate(f, samples, set_sizes); @@ -181,7 +181,7 @@ pub fn simulate Box + Send + Sync>( .map(|_| { let mut t = f(); let mut last_set_size = 0; - let mut rnd = rand::rngs::StdRng::from_entropy(); + let mut rnd = rand::rngs::StdRng::from_os_rng(); set_sizes .iter() .map(move |set_size| { diff --git a/crates/geo_filters/src/lib.rs b/crates/geo_filters/src/lib.rs index 819421b..2967454 100644 --- a/crates/geo_filters/src/lib.rs +++ b/crates/geo_filters/src/lib.rs @@ -7,13 +7,16 @@ //! Supports estimating the size of the union of two sets with a precision related to the estimated size. //! It has some similar properties as related filters like HyperLogLog, MinHash, etc, but uses less space. +pub mod build_hasher; pub mod config; pub mod diff_count; pub mod distinct_count; #[cfg(feature = "evaluation")] pub mod evaluation; +#[cfg(test)] +mod test_rng; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; /// Marker trait to indicate the variant implemented by a [`Count`] instance. pub trait Method: Clone + Eq + PartialEq + Send + Sync {} @@ -33,29 +36,51 @@ pub trait Count { /// Add the given hash to the set. fn push_hash(&mut self, hash: u64); - /// Add the hash of the given item, computed with the default hasher, to the set. - fn push(&mut self, item: I) { - self.push_hash(item_to_hash(item)) - } + /// Add the hash of the given item, computed with the configured hasher, to the set. + fn push(&mut self, item: I); /// Add the given sketch to this one. /// If only the size of the combined set is needed, [`Self::size_with_sketch`] is more efficient and should be used. fn push_sketch(&mut self, other: &Self); - /// Return the estimated set size. - fn size(&self) -> f32; + /// Return the estimated set size rounded to the nearest unsigned integer. + fn size(&self) -> usize { + let size = self.size_f32().round(); + debug_assert_f32s_in_range(size); + size as usize + } + + /// Return the estimated set size as a real number. + fn size_f32(&self) -> f32; - /// Return the estimated set size when combined with the given sketch. + /// Return the estimated set size when combined with the given sketch rounded to the nearest unsigned integer. /// If the combined set itself is not going to be used, this method is more efficient than using [`Self::push_sketch`] and [`Self::size`]. - fn size_with_sketch(&self, other: &Self) -> f32; + fn size_with_sketch(&self, other: &Self) -> usize { + let size = self.size_with_sketch_f32(other).round(); + debug_assert_f32s_in_range(size); + size as usize + } + + /// Return the estimated set size when combined with the given sketch as a real number. + /// If the combined set itself is not going to be used, this method is more efficient than using [`Self::push_sketch`] and [`Self::size`]. + fn size_with_sketch_f32(&self, other: &Self) -> f32; /// Returns the number of bytes in memory used to represent this filter. fn bytes_in_memory(&self) -> usize; } -fn item_to_hash(item: I) -> u64 { - // TODO: replace with a stable hashing function! - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - item.hash(&mut hasher); - hasher.finish() +#[inline] +fn debug_assert_f32s_in_range(v: f32) { + // The geometric filter should never produce these values. + // These assertions failing indicates that there is a bug. + debug_assert!(v.is_finite(), "Estimated size must be finite, got {v}"); + debug_assert!(v >= 0.0, "Estimated size must be non-negative, got {v}"); + debug_assert!( + v <= usize::MAX as f32, + "Estimated size {v} exceeds usize::MAX", + ); } + +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDocTests; diff --git a/crates/geo_filters/src/test_rng.rs b/crates/geo_filters/src/test_rng.rs new file mode 100644 index 0000000..7d8a8f4 --- /dev/null +++ b/crates/geo_filters/src/test_rng.rs @@ -0,0 +1,42 @@ +use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; + +use rand::SeedableRng as _; +use rand_chacha::ChaCha12Rng; + +/// Provides a seeded random number generator to tests which require some +/// degree of randomization. If the test panics the harness will print the +/// seed used for that run. You can then pass in this seed using the `TEST_SEED` +/// environment variable when running your tests. +/// +/// You can provide a number of `iterations` this harness will run with randomly +/// generated seeds. If a manual seed is provided via the environment then the test +/// is only ran once with this seed. +pub fn prng_test_harness(iterations: usize, mut test_fn: F) +where + F: FnMut(&mut ChaCha12Rng), +{ + let maybe_manual_seed = std::env::var("TEST_SEED") + .map(|s| s.parse::().expect("Parse TEST_SEED to u64")) + .ok(); + let mut seed = 0; + let maybe_panic = catch_unwind(AssertUnwindSafe(|| { + if let Some(manual_seed) = maybe_manual_seed { + seed = manual_seed; + let mut rng = ChaCha12Rng::seed_from_u64(seed); + test_fn(&mut rng); + } else { + for _ in 0..iterations { + seed = rand::random(); + let mut rng = ChaCha12Rng::seed_from_u64(seed); + test_fn(&mut rng); + } + } + })); + match maybe_panic { + Ok(t) => t, + Err(panic_info) => { + eprintln!("Test failed! Reproduce with: TEST_SEED={seed}"); + resume_unwind(panic_info); + } + } +} diff --git a/crates/geo_filters/tests/public_api.rs b/crates/geo_filters/tests/public_api.rs index d705d2d..4afff72 100644 --- a/crates/geo_filters/tests/public_api.rs +++ b/crates/geo_filters/tests/public_api.rs @@ -1,3 +1,5 @@ +use geo_filters::build_hasher::UnstableDefaultBuildHasher; + #[test] fn can_use_predefined_diff_count() { use geo_filters::diff_count::GeoDiffCount7; @@ -20,7 +22,7 @@ fn can_use_custom_diff_count() { fn can_use_diff_count_with_predefined_config_value() { use geo_filters::diff_count::{GeoDiffConfig7, GeoDiffCount}; use geo_filters::Count; - let c = GeoDiffConfig7::default(); + let c = GeoDiffConfig7::::default(); let mut f = GeoDiffCount::new(c); f.push(42); f.size(); @@ -31,7 +33,7 @@ fn can_use_diff_count_with_fixed_config_value() { use geo_filters::config::FixedConfig; use geo_filters::diff_count::GeoDiffCount; use geo_filters::Count; - let c = FixedConfig::<_, u16, 7, 128, 10>::default(); + let c = FixedConfig::<_, u16, 7, 128, 10, UnstableDefaultBuildHasher>::default(); let mut f = GeoDiffCount::new(c); f.push(42); f.size(); @@ -42,7 +44,7 @@ fn can_use_diff_count_with_variable_config_value() { use geo_filters::config::VariableConfig; use geo_filters::diff_count::GeoDiffCount; use geo_filters::Count; - let c = VariableConfig::<_, u16>::new(7, 128, 10); + let c = VariableConfig::<_, u16, UnstableDefaultBuildHasher>::new(7, 128, 10); let mut f = GeoDiffCount::new(c); f.push(42); f.size(); @@ -70,7 +72,7 @@ fn can_use_custom_distinct_count() { fn can_use_distinct_count_with_predefined_config_value() { use geo_filters::distinct_count::{GeoDistinctConfig7, GeoDistinctCount}; use geo_filters::Count; - let c = GeoDistinctConfig7::default(); + let c = GeoDistinctConfig7::::default(); let mut f = GeoDistinctCount::new(c); f.push(42); f.size(); @@ -81,7 +83,7 @@ fn can_use_distinct_count_with_fixed_config_value() { use geo_filters::config::FixedConfig; use geo_filters::distinct_count::GeoDistinctCount; use geo_filters::Count; - let c = FixedConfig::<_, u16, 7, 118, 11>::default(); + let c = FixedConfig::<_, u16, 7, 118, 11, UnstableDefaultBuildHasher>::default(); let mut f = GeoDistinctCount::new(c); f.push(42); f.size(); @@ -92,7 +94,7 @@ fn can_use_distinct_count_with_variable_config_value() { use geo_filters::config::VariableConfig; use geo_filters::distinct_count::GeoDistinctCount; use geo_filters::Count; - let c = VariableConfig::<_, u16>::new(7, 118, 11); + let c = VariableConfig::<_, u16, UnstableDefaultBuildHasher>::new(7, 118, 11); let mut f = GeoDistinctCount::new(c); f.push(42); f.size(); diff --git a/crates/string-offsets/.gitignore b/crates/string-offsets/.gitignore new file mode 100644 index 0000000..8727140 --- /dev/null +++ b/crates/string-offsets/.gitignore @@ -0,0 +1,3 @@ +node_modules +pkg +.tgz diff --git a/crates/string-offsets/CONTRIBUTING.md b/crates/string-offsets/CONTRIBUTING.md new file mode 100644 index 0000000..c47c511 --- /dev/null +++ b/crates/string-offsets/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +## Building the WASM/JS package + +The code for the wasm + js wrapper package is stored in the `js` directory. To build it: + +```sh +cd js +npm i +npm run compile +``` + +The js code will be output to `js/pkg`. + +To run a quick sanity check of the JS package: + +```sh +npm test +``` + +To publish the package to npm, run: + +```sh +cd js +npm publish +``` diff --git a/crates/string-offsets/Cargo.toml b/crates/string-offsets/Cargo.toml index fd9b838..e20db3b 100644 --- a/crates/string-offsets/Cargo.toml +++ b/crates/string-offsets/Cargo.toml @@ -1,14 +1,30 @@ [package] name = "string-offsets" authors = ["The blackbird team "] -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "Converts string offsets between UTF-8 bytes, UTF-16 code units, Unicode code points, and lines." repository = "https://github.com/github/rust-gems" license = "MIT" keywords = ["unicode", "positions", "utf16", "characters", "lines"] categories = ["algorithms", "data-structures", "text-processing", "development-tools::ffi"] +exclude = ["/js"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +wasm = ["wasm-bindgen"] + +[dependencies] +wasm-bindgen = { version = "0.2", optional = true } [dev-dependencies] -rand = "0.8" -rand_chacha = "0.3" +rand = "0.9" +rand_chacha = "0.9" +criterion = "0.7" + +[[bench]] +name = "performance" +path = "benchmarks/performance.rs" +harness = false diff --git a/crates/string-offsets/benchmarks/performance.rs b/crates/string-offsets/benchmarks/performance.rs new file mode 100644 index 0000000..9c6bc41 --- /dev/null +++ b/crates/string-offsets/benchmarks/performance.rs @@ -0,0 +1,46 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use rand::{rng, Rng}; +use string_offsets::{AllConfig, OnlyLines, StringOffsets}; + +fn only_lines_construction_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("only_lines_construction"); + for size in [1000, 10000, 100000] { + let mut rng = rng(); + // Generate random ascii input. + let random_input: String = (0..size) + .map(|_| rng.random_range(32u8..128u8) as char) + .collect(); + group.throughput(criterion::Throughput::Bytes(random_input.len() as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(size), + &random_input, + |b, input| b.iter(|| black_box(StringOffsets::::new(input))), + ); + } + group.finish(); +} + +fn full_construction_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("full_construction"); + for size in [1000, 10000, 100000] { + let mut rng = rng(); + // Generate random ascii input. + let random_input: String = (0..size) + .map(|_| rng.random_range(32u8..128u8) as char) + .collect(); + group.throughput(criterion::Throughput::Bytes(random_input.len() as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(size), + &random_input, + |b, input| b.iter(|| black_box(StringOffsets::::new(input))), + ); + } + group.finish(); +} + +criterion_group!( + name = benches; + config = Criterion::default(); + targets = only_lines_construction_benchmark, full_construction_benchmark +); +criterion_main!(benches); diff --git a/crates/string-offsets/js/.npmignore b/crates/string-offsets/js/.npmignore new file mode 100644 index 0000000..419b728 --- /dev/null +++ b/crates/string-offsets/js/.npmignore @@ -0,0 +1,4 @@ +/* +!/pkg +pkg/*/package.json +pkg/*/README.md diff --git a/crates/string-offsets/js/package-lock.json b/crates/string-offsets/js/package-lock.json new file mode 100644 index 0000000..de0e02c --- /dev/null +++ b/crates/string-offsets/js/package-lock.json @@ -0,0 +1,3870 @@ +{ + "name": "string-offsets", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "string-offsets", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.0.0", + "wasm-pack": "^0.13.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-install": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.1.0.tgz", + "integrity": "sha512-rkwNGW+3aQVSZoD0/o3mfPN6Yxh3Id0R/xzTVBVVpGNlVz8EGwusksxRlbk/A5iKTZt9zkMn3qIqmAt3vpfbzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.26.1", + "rimraf": "^3.0.2", + "tar": "^6.1.11" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001704", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", + "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.116", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.116.tgz", + "integrity": "sha512-mufxTCJzLBQVvSdZzX1s5YAuXsN1M4tTyYxOOL1TcSKtIzQ9rjIrm7yFK80rN5dwGTePgdoABDSHpuVtRQh0Zw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wasm-pack": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.13.1.tgz", + "integrity": "sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "binary-install": "^1.0.1" + }, + "bin": { + "wasm-pack": "run.js" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/crates/string-offsets/js/package.json b/crates/string-offsets/js/package.json new file mode 100644 index 0000000..758aea7 --- /dev/null +++ b/crates/string-offsets/js/package.json @@ -0,0 +1,34 @@ +{ + "name": "string-offsets", + "version": "0.1.0", + "author": "The blackbird team ", + "license": "MIT", + "description": "String offset conversions between UTF-8, UTF-16, and lines", + "keywords": [ + "string", + "utf8", + "utf16", + "wasm", + "rust" + ], + "main": "pkg/nodejs/string_offsets.js", + "types": "pkg/nodejs/string_offsets.d.ts", + "exports": { + "bundler": "./pkg/bundler/string_offsets.js", + "node": "./pkg/nodejs/string_offsets.js", + "import": "./pkg/web/string_offsets.js", + "require": "./pkg/nodejs/string_offsets.js" + }, + "scripts": { + "compile:bundler": "wasm-pack build --target bundler -d js/pkg/bundler --features wasm && node -e \"fs.unlinkSync('./pkg/bundler/.gitignore')\"", + "compile:web": "wasm-pack build --target web -d js/pkg/web --features wasm && node -e \"fs.unlinkSync('./pkg/web/.gitignore')\"", + "compile:nodejs": "wasm-pack build --target nodejs -d js/pkg/nodejs --features wasm && node -e \"fs.unlinkSync('./pkg/nodejs/.gitignore')\"", + "compile": "npm run compile:web && npm run compile:bundler && npm run compile:nodejs", + "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.0.0", + "wasm-pack": "^0.13.1" + } +} diff --git a/crates/string-offsets/js/test/test.js b/crates/string-offsets/js/test/test.js new file mode 100644 index 0000000..bb66ed8 --- /dev/null +++ b/crates/string-offsets/js/test/test.js @@ -0,0 +1,23 @@ +//@ts-check +const { StringOffsets } = require('../'); + +describe('StringOffsets sanity checks', () => { + test('basic ASCII text', () => { + const text = "hello\nworld"; + const offsets = new StringOffsets(text); + + expect(offsets.lines()).toBe(2); + expect(offsets.utf8ToUtf16(0)).toBe(0); + expect(offsets.utf8ToLine(0)).toBe(0); + }); + + test('Unicode text', () => { + const text = "☀️hello\n🗺️world"; + const offsets = new StringOffsets(text); + + expect(offsets.lines()).toBe(2); + // ☀️ is 6 UTF-8 bytes and 3 UTF-16 code units + expect(offsets.utf8ToUtf16(6)).toBe(2); + expect(offsets.utf8ToUtf16(0)).toBe(0); + }); +}); diff --git a/crates/string-offsets/src/bitrank.rs b/crates/string-offsets/src/bitrank.rs index 6524769..8acc3c4 100644 --- a/crates/string-offsets/src/bitrank.rs +++ b/crates/string-offsets/src/bitrank.rs @@ -6,9 +6,9 @@ type SubblockBits = u128; // Static sizing of the various components of the data structure. -const BITS_PER_BLOCK: usize = 16384; const BITS_PER_SUB_BLOCK: usize = SubblockBits::BITS as usize; -const SUB_BLOCKS_PER_BLOCK: usize = BITS_PER_BLOCK / BITS_PER_SUB_BLOCK; +const SUB_BLOCKS_PER_BLOCK: usize = 64; +const BITS_PER_BLOCK: usize = SUB_BLOCKS_PER_BLOCK * BITS_PER_SUB_BLOCK; // 8192 Bits = 1 kBytes /// A container for a portion of the total bit vector and the associated indices. /// The bits within each chunk are stored from most significant bit (msb) to least significant bit (lsb). @@ -40,24 +40,17 @@ struct Block { impl Block { /// Set a bit without updating `self.sub_blocks`. - /// - /// This panics if the bit was already set, because that indicates that the original positions - /// list is invalid/had duplicates. fn set(&mut self, index: usize) { - assert!(index < BITS_PER_BLOCK); + debug_assert!(index < BITS_PER_BLOCK); let chunk_idx = index / BITS_PER_SUB_BLOCK; let bit_idx = index % BITS_PER_SUB_BLOCK; - let mask = 1 << ((BITS_PER_SUB_BLOCK - 1) - bit_idx); - assert_eq!(self.bits[chunk_idx] & mask, 0, "toggling bits off indicates that the original data was incorrect, most likely containing duplicate values."); - self.bits[chunk_idx] ^= mask; + let mask = 1 << bit_idx; + debug_assert_eq!(self.bits[chunk_idx] & mask, 0, "toggling bits off indicates that the original data was incorrect, most likely containing duplicate values."); + self.bits[chunk_idx] |= mask; } - /// The **total rank** of the block relative local index, and the index of the one - /// bit that establishes that rank (aka "select") **if** it occurs within that same - /// chunk, otherwise ['None']. The assumption is that if you would have to look back - /// through previous chunks it would actually be cheaper to do a lookup in the original - /// data structure that the bit vector was created from. - fn rank_select(&self, local_idx: usize) -> (usize, Option) { + /// The **total rank** of the block relative local index. + fn rank(&self, local_idx: usize) -> usize { let mut rank = self.rank as usize; let sub_block = local_idx / BITS_PER_SUB_BLOCK; rank += self.sub_blocks[sub_block] as usize; @@ -65,18 +58,8 @@ impl Block { let remainder = local_idx % BITS_PER_SUB_BLOCK; let last_chunk = local_idx / BITS_PER_SUB_BLOCK; - let masked = if remainder == 0 { - 0 - } else { - self.bits[last_chunk] >> (BITS_PER_SUB_BLOCK - remainder) - }; - rank += masked.count_ones() as usize; - let select = if masked == 0 { - None - } else { - Some(local_idx - masked.trailing_zeros() as usize - 1) - }; - (rank, select) + let masked = self.bits[last_chunk] & !(SubblockBits::MAX << remainder); + rank + masked.count_ones() as usize } fn total_rank(&self) -> usize { @@ -107,62 +90,36 @@ pub struct BitRankBuilder { } impl BitRankBuilder { - /// Returns a new builder. - #[cfg(test)] - pub fn new() -> Self { - Self::default() - } - /// Returns a builder that can hold integers with values `0..cap`. pub fn with_capacity(cap: usize) -> Self { + const ZERO_BLOCK: Block = Block { + rank: 0, + sub_blocks: [0; SUB_BLOCKS_PER_BLOCK], + bits: [0; SUB_BLOCKS_PER_BLOCK], + }; Self { - blocks: Vec::with_capacity(cap.div_ceil(BITS_PER_BLOCK)), - } - } - - fn finish_last_block(&mut self) -> u64 { - if let Some(block) = self.blocks.last_mut() { - let mut local_rank = 0; - for (i, chunk) in block.bits.iter().enumerate() { - block.sub_blocks[i] = local_rank; - local_rank += chunk.count_ones() as u16; - } - block.rank + local_rank as u64 - } else { - 0 + blocks: vec![ZERO_BLOCK; cap.div_ceil(BITS_PER_BLOCK)], } } /// Adds a bit. Bits must be added in order of increasing `position`. pub fn push(&mut self, position: usize) { let block_id = position / BITS_PER_BLOCK; - assert!( - self.blocks.len() <= block_id + 1, - "positions must be increasing!" - ); - if block_id >= self.blocks.len() { - let curr_rank = self.finish_last_block(); - while block_id >= self.blocks.len() { - // Without this declared as a `const`, rustc 1.82 creates the Block value on the - // stack first, then `memcpy`s it into `self.blocks`. - const ZERO_BLOCK: Block = Block { - rank: 0, - sub_blocks: [0; SUB_BLOCKS_PER_BLOCK], - bits: [0; SUB_BLOCKS_PER_BLOCK], - }; - self.blocks.push(ZERO_BLOCK); - self.blocks.last_mut().expect("just inserted").rank = curr_rank; - } - } - self.blocks - .last_mut() - .expect("just ensured there are enough blocks") - .set(position % BITS_PER_BLOCK); + self.blocks[block_id].set(position % BITS_PER_BLOCK); } /// Finishes the `BitRank` by writing the last block of data. pub fn finish(mut self) -> BitRank { - self.finish_last_block(); + let mut total_rank = 0; + for block in &mut self.blocks { + block.rank = total_rank; + let mut local_rank = 0; + for (i, chunk) in block.bits.iter().enumerate() { + block.sub_blocks[i] = local_rank; + local_rank += chunk.count_ones() as u16; + } + total_rank += local_rank as u64 + } BitRank { blocks: self.blocks, } @@ -181,7 +138,12 @@ impl BitRank { /// The (one) rank is defined as: `rank(i) = sum(b[j] for j in 0..i)` /// i.e. the number of elements less than `i`. pub fn rank(&self, idx: usize) -> usize { - self.rank_select(idx).0 + let block_num = idx / BITS_PER_BLOCK; + if block_num >= self.blocks.len() { + self.max_rank() // fall back to 0 bits when the bitrank data structure is empty. + } else { + self.blocks[block_num].rank(idx % BITS_PER_BLOCK) + } } /// Returns the number of elements in the set. @@ -191,43 +153,34 @@ impl BitRank { .map(|b| b.total_rank()) .unwrap_or_default() // fall back to 0 when the bitrank data structure is empty. } - - /// The rank at the specified index(exclusive) and the index of the one bit that - /// establishes that rank (aka "select") **if** it occurs within that same chunk, - /// otherwise ['None']. The assumption is that if you would have to look back - /// through previous chunks it would actually be cheaper to do a lookup in the original - /// data structure that the bit vector was created from. - pub fn rank_select(&self, idx: usize) -> (usize, Option) { - let block_num = idx / BITS_PER_BLOCK; - // assert!(block_num < self.blocks.len(), "index out of bounds"); - if block_num >= self.blocks.len() { - ( - self.max_rank(), // fall back to 0 when the bitrank data structure is empty. - None, - ) - } else { - let (rank, b_idx) = self.blocks[block_num].rank_select(idx % BITS_PER_BLOCK); - (rank, b_idx.map(|i| (block_num * BITS_PER_BLOCK) + i)) - } - } } #[cfg(test)] mod tests { - use rand::distributions::Uniform; + use super::*; + use rand::distr::Uniform; use rand::prelude::*; + use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha8Rng; - use super::*; - /// Creates a `BitRank` containing the integers in `iter` (which should be strictly /// increasing). - pub fn bitrank>(iter: I) -> BitRank { - let mut builder = BitRankBuilder::new(); - for position in iter { - builder.push(position); + pub fn bitrank(iter: I) -> BitRank + where + I: IntoIterator, + I::IntoIter: DoubleEndedIterator, + { + let mut iter = iter.into_iter().rev(); + if let Some(last) = iter.next() { + let mut builder = BitRankBuilder::with_capacity(last + 1); + builder.push(last); + for position in iter { + builder.push(position); + } + builder.finish() + } else { + BitRank { blocks: vec![] } } - builder.finish() } #[test] @@ -282,29 +235,29 @@ mod tests { let mut positions: Vec = (0..132).collect(); positions.append(&mut vec![138usize, 140, 146]); let br = bitrank(positions); - assert_eq!(br.rank_select(135), (132, Some(131))); + assert_eq!(br.rank(135), 132); let bits2: Vec = (0..BITS_PER_BLOCK - 5).collect(); let br2 = bitrank(bits2); - assert_eq!(br2.rank_select(169), (169, Some(168))); + assert_eq!(br2.rank(169), 169); let bits3: Vec = (0..BITS_PER_BLOCK + 5).collect(); let br3 = bitrank(bits3); - assert_eq!(br3.rank_select(BITS_PER_BLOCK), (BITS_PER_BLOCK, None)); + assert_eq!(br3.rank(BITS_PER_BLOCK), BITS_PER_BLOCK); - let bits4: Vec = vec![1, 1000, 9999, BITS_PER_BLOCK + 1]; + let bits4: Vec = vec![1, 1000, 7777, BITS_PER_BLOCK + 1]; let br4 = bitrank(bits4); - assert_eq!(br4.rank_select(10000), (3, Some(9999))); + assert_eq!(br4.rank(8000), 3); - let bits5: Vec = vec![1, 1000, 9999, BITS_PER_BLOCK + 1]; + let bits5: Vec = vec![1, 1000, 7777, BITS_PER_BLOCK + 1]; let br5 = bitrank(bits5); - assert_eq!(br5.rank_select(BITS_PER_BLOCK), (3, None)); + assert_eq!(br5.rank(BITS_PER_BLOCK), 3); } #[test] fn test_rank_large_random() { let mut rng = ChaCha8Rng::seed_from_u64(2); - let uniform = Uniform::::from(0..1_000_000); + let uniform = Uniform::new(0, 1_000_000).unwrap(); let mut random_bits = Vec::with_capacity(100_000); for _ in 0..100_000 { random_bits.push(uniform.sample(&mut rng)); @@ -315,15 +268,10 @@ mod tests { random_bits.dedup(); let br = bitrank(random_bits.iter().copied()); let mut rank = 0; - let mut select = None; for i in 0..random_bits.capacity() { - if i % BITS_PER_SUB_BLOCK == 0 { - select = None; - } - assert_eq!(br.rank_select(i), (rank, select)); + assert_eq!(br.rank(i), rank); if i == random_bits[rank] { rank += 1; - select = Some(i); } } } diff --git a/crates/string-offsets/src/config.rs b/crates/string-offsets/src/config.rs new file mode 100644 index 0000000..33b99a4 --- /dev/null +++ b/crates/string-offsets/src/config.rs @@ -0,0 +1,50 @@ +//! Configuration types for enabling/disabling features are compile time. +//! +//! By disabling features, the compiler can generate faster code which can be important for certain use cases. +//! Certain implementations/conversion operations will only be available if the corresponding features were enabled. + +/// Type-level boolean. +pub trait Bool { + /// The value of the boolean. + const VALUE: bool; +} +/// Type-level true. +pub struct True {} +/// Type-level false. +pub struct False {} +impl Bool for True { + const VALUE: bool = true; +} +impl Bool for False { + const VALUE: bool = false; +} + +/// Configures which features should be enabled for a [`StringOffsets`] instance. +pub trait ConfigType { + /// Whether to enable character conversions. + type HasChars: Bool; + /// Whether to enable UTF-16 conversions. + type HasUtf16: Bool; + /// Whether to enable line conversions. + type HasLines: Bool; + /// Whether to enable whitespace checks. + type HasWhitespace: Bool; +} + +/// Configuration type that enables all features. +pub struct AllConfig {} +impl ConfigType for AllConfig { + type HasChars = True; + type HasUtf16 = True; + type HasLines = True; + type HasWhitespace = True; +} + +/// Configuration type that only enables line conversions. +pub struct OnlyLines {} +impl ConfigType for OnlyLines { + type HasChars = False; + type HasUtf16 = False; + type HasLines = True; + type HasWhitespace = False; +} diff --git a/crates/string-offsets/src/lib.rs b/crates/string-offsets/src/lib.rs index ee05e54..35900ad 100644 --- a/crates/string-offsets/src/lib.rs +++ b/crates/string-offsets/src/lib.rs @@ -6,7 +6,7 @@ //! use string_offsets::StringOffsets; //! //! let s = "☀️hello\n🗺️world\n"; -//! let offsets = StringOffsets::new(s); +//! let offsets: StringOffsets = StringOffsets::new(s); //! //! // Find offsets where lines begin and end. //! assert_eq!(offsets.line_to_utf8s(0), 0..12); // note: 0-based line numbers @@ -24,11 +24,20 @@ //! See [`StringOffsets`] for details. #![deny(missing_docs)] -use std::ops::Range; +use std::{marker::PhantomData, ops::Range}; + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; mod bitrank; +mod config; +#[cfg(feature = "wasm")] +mod wasm; use bitrank::{BitRank, BitRankBuilder}; +use config::{Bool, ConfigType, True}; + +pub use config::{AllConfig, OnlyLines}; /// Converts positions within a given string between UTF-8 byte offsets (the usual in Rust), UTF-16 /// code units, Unicode code points, and line numbers. @@ -84,7 +93,9 @@ use bitrank::{BitRank, BitRankBuilder}; /// Most operations run in O(1) time. A few require O(log n) time. The memory consumed by this /// data structure is typically less than the memory occupied by the actual content. In the best /// case, it requires ~45% of the content space. -pub struct StringOffsets { +/// One can reduce memory requirements further by only requesting the necessary features via the +/// configuration type. +pub struct StringOffsets { /// Vector storing, for every line, the byte position at which the line starts. line_begins: Vec, @@ -92,19 +103,25 @@ pub struct StringOffsets { /// the byte belongs. utf8_to_line: BitRank, - /// Encoded bitrank where the rank of a byte position corresponds to the char position to which + /// Encoded bitrank where the start of a utf8 code point is marked with a 1 bit. + /// The rank of a byte position + 1 corresponds to the char position + 1 to which /// the byte belongs. utf8_to_char: BitRank, - /// Encoded bitrank where the rank of a byte position corresponds to the UTF-16 encoded word - /// position to which the byte belongs. + /// Encoded bitrank where a multi word utf16 code point is marked with a 1 bit. + /// Converting a byte position into a utf16 word position is achieved by combining utf8_to_char + /// and utf8_to_utf16 rank information. utf8_to_utf16: BitRank, /// Marks, for every line, whether it consists only of whitespace characters. whitespace_only: Vec, + + /// Configuration type. + _config: PhantomData, } /// A position in a string, specified by line and column number. +#[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Pos { /// Zero-indexed line number. @@ -132,7 +149,7 @@ pub struct Pos { // Question: Consider whether we should return an empty line range in this case which would // probably be consistent from a mathematical point of view. But then we should also return empty // line ranges for empty character ranges in the middle of a line... -impl StringOffsets { +impl StringOffsets { /// Create a new converter to work with offsets into the given string. pub fn new(content: &str) -> Self { new_converter(content.as_bytes()) @@ -145,11 +162,17 @@ impl StringOffsets { pub fn from_bytes(content: &[u8]) -> Self { new_converter(content) } +} - /// Returns the number of Unicode characters on the specified line. - pub fn line_chars(&self, line_number: usize) -> usize { - let r = self.utf8s_to_chars(self.line_to_utf8s(line_number)); - r.end - r.start +impl> StringOffsets { + /// Returns the number of bytes in the string. + pub fn len(&self) -> usize { + self.line_begins.last().copied().unwrap_or(0) as usize + } + + /// Returns whether there are no bytes in the string. + pub fn is_empty(&self) -> bool { + self.line_begins.is_empty() } /// Returns the number of lines in the string. @@ -157,14 +180,6 @@ impl StringOffsets { self.line_begins.len() - 1 } - /// Returns true if the specified line is empty except for whitespace. - pub fn only_whitespaces(&self, line_number: usize) -> bool { - self.whitespace_only - .get(line_number) - .copied() - .unwrap_or(true) - } - /// Return the byte offset of the first character on the specified (zero-based) line. /// /// If `line_number` is greater than or equal to the number of lines in the text, this returns @@ -173,12 +188,49 @@ impl StringOffsets { self.line_begins[line_number.min(self.lines())] as usize } - /// UTF-16 offset of the first character of a line. + /// UTF-8 offset of the first character of a line. + pub fn line_to_utf8_end(&self, line_number: usize) -> usize { + self.line_to_utf8_begin(line_number + 1) + } + + /// Return the zero-based line number of the line containing the specified UTF-8 offset. + /// Newline characters count as part of the preceding line. + pub fn utf8_to_line(&self, byte_number: usize) -> usize { + self.utf8_to_line.rank(byte_number) + } + + /// Returns the range of line numbers containing the substring specified by the Rust-style + /// range `bytes`. Newline characters count as part of the preceding line. /// - /// That is, return the offset that would point to the start of that line in a UTF-16 - /// representation of the source string. - pub fn line_to_utf16_begin(&self, line_number: usize) -> usize { - self.utf8_to_utf16(self.line_to_utf8_begin(line_number)) + /// If `bytes` is an empty range at a position within or at the beginning of a line, this + /// returns a nonempty range containing the line number of that one line. An empty range at or + /// beyond the end of the string translates to an empty range of line numbers. + pub fn utf8s_to_lines(&self, bytes: Range) -> Range { + // The fiddly parts of this formula are necessary because `bytes.start` rounds down to the + // beginning of the line, but `bytes.end` "rounds up" to the end of the line. the final + // `+1` is to produce a half-open range. + self.utf8_to_line(bytes.start) + ..self + .lines() + .min(self.utf8_to_line(bytes.end.saturating_sub(1).max(bytes.start)) + 1) + } + + /// UTF-8 offset one past the end of a line (the offset of the start of the next line). + pub fn line_to_utf8s(&self, line_number: usize) -> Range { + self.line_to_utf8_begin(line_number)..self.line_to_utf8_end(line_number) + } + + /// UTF-8 offsets for the beginning and end of a range of lines, including the newline if any. + pub fn lines_to_utf8s(&self, line_numbers: Range) -> Range { + self.line_to_utf8_begin(line_numbers.start)..self.line_to_utf8_begin(line_numbers.end) + } +} + +impl> StringOffsets { + /// Returns the number of Unicode characters on the specified line. + pub fn line_chars(&self, line_number: usize) -> usize { + let r = self.utf8s_to_chars(self.line_to_utf8s(line_number)); + r.end - r.start } /// UTF-32 offset of the first character of a line. @@ -189,47 +241,21 @@ impl StringOffsets { self.utf8_to_char(self.line_to_utf8_begin(line_number)) } - /// UTF-8 offset of the first character of a line. - pub fn line_to_utf8_end(&self, line_number: usize) -> usize { - self.line_to_utf8_begin(line_number + 1) - } - - /// UTF-16 offset one past the end of a line (the offset of the start of the next line). - pub fn line_to_utf16_end(&self, line_number: usize) -> usize { - self.utf8_to_utf16(self.line_to_utf8_end(line_number)) - } - /// UTF-32 offset one past the end of a line (the offset of the start of the next line). pub fn line_to_char_end(&self, line_number: usize) -> usize { self.utf8_to_char(self.line_to_utf8_end(line_number)) } - /// UTF-8 offset one past the end of a line (the offset of the start of the next line). - pub fn line_to_utf8s(&self, line_number: usize) -> Range { - self.line_to_utf8_begin(line_number)..self.line_to_utf8_end(line_number) - } - /// UTF-32 offsets for the beginning and end of a line, including the newline if any. pub fn line_to_chars(&self, line_number: usize) -> Range { self.utf8s_to_chars(self.line_to_utf8s(line_number)) } - /// UTF-8 offsets for the beginning and end of a range of lines, including the newline if any. - pub fn lines_to_utf8s(&self, line_numbers: Range) -> Range { - self.line_to_utf8_begin(line_numbers.start)..self.line_to_utf8_begin(line_numbers.end) - } - /// UTF-32 offsets for the beginning and end of a range of lines, including the newline if any. pub fn lines_to_chars(&self, line_numbers: Range) -> Range { self.utf8s_to_chars(self.lines_to_utf8s(line_numbers)) } - /// Return the zero-based line number of the line containing the specified UTF-8 offset. - /// Newline characters count as part of the preceding line. - pub fn utf8_to_line(&self, byte_number: usize) -> usize { - self.utf8_to_line.rank(byte_number) - } - /// Converts a UTF-8 offset to a zero-based line number and UTF-32 offset within the /// line. pub fn utf8_to_char_pos(&self, byte_number: usize) -> Pos { @@ -242,48 +268,27 @@ impl StringOffsets { } } - /// Converts a UTF-8 offset to a zero-based line number and UTF-16 offset within the - /// line. - pub fn utf8_to_utf16_pos(&self, byte_number: usize) -> Pos { - let line = self.utf8_to_line(byte_number); - let line_start_char_number = self.line_to_utf16_begin(line); - let char_idx = self.utf8_to_utf16(byte_number); - Pos { - line, - col: char_idx - line_start_char_number, - } - } - - /// Returns the range of line numbers containing the substring specified by the Rust-style - /// range `bytes`. Newline characters count as part of the preceding line. - /// - /// If `bytes` is an empty range at a position within or at the beginning of a line, this - /// returns a nonempty range containing the line number of that one line. An empty range at or - /// beyond the end of the string translates to an empty range of line numbers. - pub fn utf8s_to_lines(&self, bytes: Range) -> Range { - // The fiddly parts of this formula are necessary because `bytes.start` rounds down to the - // beginning of the line, but `bytes.end` "rounds up" to the end of the line. the final - // `+1` is to produce a half-open range. - self.utf8_to_line(bytes.start) - ..self - .lines() - .min(self.utf8_to_line(bytes.end.saturating_sub(1).max(bytes.start)) + 1) - } - /// Returns the range of line numbers containing the substring specified by the UTF-32 /// range `chars`. Newline characters count as part of the preceding line. pub fn chars_to_lines(&self, chars: Range) -> Range { self.utf8s_to_lines(self.chars_to_utf8s(chars)) } +} - /// Converts a UTF-8 offset to a UTF-32 offset. - pub fn utf8_to_char(&self, byte_number: usize) -> usize { - self.utf8_to_char.rank(byte_number) +impl> StringOffsets { + /// Returns true if the specified line is empty except for whitespace. + pub fn only_whitespaces(&self, line_number: usize) -> bool { + self.whitespace_only + .get(line_number) + .copied() + .unwrap_or(true) } +} - /// Converts a UTF-8 offset to a UTF-16 offset. - pub fn utf8_to_utf16(&self, byte_number: usize) -> usize { - self.utf8_to_utf16.rank(byte_number) +impl> StringOffsets { + /// Converts a UTF-8 offset to a UTF-32 offset. + pub fn utf8_to_char(&self, byte_number: usize) -> usize { + self.utf8_to_char.rank(byte_number + 1) - 1 } /// Converts a UTF-32 offset to a UTF-8 offset. @@ -299,7 +304,7 @@ impl StringOffsets { // If we couldn't find the char within 128 steps, then the char_number might be invalid! // This does not usually happen. For consistency with the rest of the code, we simply return // the max utf8 position in this case. - if char_number > self.utf8_to_char.max_rank() { + if char_number >= self.utf8_to_char.max_rank() { return self .line_begins .last() @@ -330,46 +335,81 @@ impl StringOffsets { } } -fn new_converter(content: &[u8]) -> StringOffsets { +impl> StringOffsets { + /// Converts a UTF-8 offset to a UTF-16 offset. + pub fn utf8_to_utf16(&self, byte_number: usize) -> usize { + self.utf8_to_char(byte_number) + self.utf8_to_utf16.rank(byte_number) + } +} + +impl> StringOffsets { + /// UTF-16 offset of the first character of a line. + /// + /// That is, return the offset that would point to the start of that line in a UTF-16 + /// representation of the source string. + pub fn line_to_utf16_begin(&self, line_number: usize) -> usize { + self.utf8_to_utf16(self.line_to_utf8_begin(line_number)) + } + + /// UTF-16 offset one past the end of a line (the offset of the start of the next line). + pub fn line_to_utf16_end(&self, line_number: usize) -> usize { + self.utf8_to_utf16(self.line_to_utf8_end(line_number)) + } + + /// Converts a UTF-8 offset to a zero-based line number and UTF-16 offset within the + /// line. + pub fn utf8_to_utf16_pos(&self, byte_number: usize) -> Pos { + let line = self.utf8_to_line(byte_number); + let line_start_char_number = self.line_to_utf16_begin(line); + let char_idx = self.utf8_to_utf16(byte_number); + Pos { + line, + col: char_idx - line_start_char_number, + } + } +} + +fn new_converter(content: &[u8]) -> StringOffsets { let n = content.len(); - let mut utf8_builder = BitRankBuilder::with_capacity(n); - let mut utf16_builder = BitRankBuilder::with_capacity(n); - let mut line_builder = BitRankBuilder::with_capacity(n); + let mut utf8_builder = + BitRankBuilder::with_capacity(if C::HasChars::VALUE { n + 1 } else { 0 }); + let mut utf16_builder = BitRankBuilder::with_capacity(if C::HasUtf16::VALUE { n } else { 0 }); + let mut line_builder = BitRankBuilder::with_capacity(if C::HasLines::VALUE { n } else { 0 }); let mut line_begins = vec![0]; - let mut i = 0; let mut whitespace_only = vec![]; let mut only_whitespaces = true; // true if all characters in the current line are whitespaces. - while i < content.len() { - // In case of invalid utf8, we might get a utf8_len of 0. - // In this case, we just treat the single byte character. - // In principle, a single incorrect byte can break the whole decoding... - let c = content[i]; - let utf8_len = utf8_width(c).max(1); - if i > 0 { - utf8_builder.push(i - 1); - utf16_builder.push(i - 1); + for (i, &c) in content.iter().enumerate() { + // Note: We expect here proper utf8 encoded strings! Otherwise, the conversion will have undefined behaviour. + if C::HasChars::VALUE && is_char_boundary(c) { + utf8_builder.push(i); } - if utf8_to_utf16_width(&content[i..]) > 1 { + if C::HasUtf16::VALUE && two_utf16(c) { utf16_builder.push(i); } if c == b'\n' { - whitespace_only.push(only_whitespaces); - line_begins.push(i as u32 + 1); - line_builder.push(i); - only_whitespaces = true; // reset for next line. - } else { - only_whitespaces &= matches!(c, b'\t' | b'\r' | b' '); + if C::HasWhitespace::VALUE { + whitespace_only.push(only_whitespaces); + only_whitespaces = true; // reset for next line. + } + if C::HasLines::VALUE { + line_begins.push(i as u32 + 1); + line_builder.push(i); + } + } else if C::HasWhitespace::VALUE { + only_whitespaces = only_whitespaces && matches!(c, b'\t' | b'\r' | b' '); } - i += utf8_len; } - if !content.is_empty() { - utf8_builder.push(content.len() - 1); - utf16_builder.push(content.len() - 1); + if C::HasChars::VALUE { + utf8_builder.push(n); } - if line_begins.last() != Some(&(content.len() as u32)) { - whitespace_only.push(only_whitespaces); - line_begins.push(content.len() as u32); - line_builder.push(content.len() - 1); + if line_begins.last() != Some(&(n as u32)) { + if C::HasWhitespace::VALUE { + whitespace_only.push(only_whitespaces); + } + if C::HasLines::VALUE { + line_begins.push(n as u32); + line_builder.push(n - 1); + } } StringOffsets { @@ -378,34 +418,39 @@ fn new_converter(content: &[u8]) -> StringOffsets { whitespace_only, utf8_to_char: utf8_builder.finish(), utf8_to_utf16: utf16_builder.finish(), + _config: PhantomData, } } -/// Returns the number of bytes a UTF-8 char occupies, given the first byte of the UTF-8 encoding. -/// Returns 0 if the byte is not a valid first byte of a UTF-8 char. -fn utf8_width(c: u8) -> usize { - // Every nibble represents the utf8 length given the first 4 bits of a utf8 encoded byte. - const UTF8_WIDTH: usize = 0x4322_0000_1111_1111; - (UTF8_WIDTH >> ((c >> 4) * 4)) & 0xf +/// Returns true if, in a UTF-8 string, `b` indicates the first byte of a character. +fn is_char_boundary(b: u8) -> bool { + b as i8 >= -0x40 // NB: b < 128 || b >= 192 } -fn utf8_to_utf16_width(content: &[u8]) -> usize { - let len = utf8_width(content[0]); - match len { - 0 => 0, - 1..=3 => 1, - 4 => 2, - _ => panic!("invalid utf8 char width: {}", len), - } +fn two_utf16(c: u8) -> bool { + c & 0b1111_0000 == 0b1111_0000 } #[cfg(test)] mod tests { use super::*; - /// Returns true if, in a UTF-8 string, `b` indicates the first byte of a character. - fn is_char_boundary(b: u8) -> bool { - b as i8 >= -0x40 // NB: b < 128 || b >= 192 + /// Returns the number of bytes a UTF-8 char occupies, given the first byte of the UTF-8 encoding. + /// Returns 0 if the byte is not a valid first byte of a UTF-8 char. + fn utf8_width(c: u8) -> usize { + // Every nibble represents the utf8 length given the first 4 bits of a utf8 encoded byte. + const UTF8_WIDTH: u64 = 0x4322_0000_1111_1111; + ((UTF8_WIDTH >> ((c >> 4) * 4)) & 0xf) as usize + } + + fn utf8_to_utf16_width(content: &[u8]) -> usize { + let len = utf8_width(content[0]); + match len { + 0 => 0, + 1..=3 => 1, + 4 => 2, + _ => panic!("invalid utf8 char width: {len}"), + } } #[test] @@ -445,7 +490,7 @@ mod tests { let content = r#"a short line. followed by another one. no terminating newline!"#; - let lines = StringOffsets::new(content); + let lines: StringOffsets = StringOffsets::new(content); assert_eq!(lines.line_to_utf8s(0), 0..14); assert_eq!(&content[0..14], "a short line.\n"); assert_eq!(lines.line_to_utf8s(1), 14..39); @@ -491,7 +536,7 @@ no terminating newline!"#; fn test_convert_ascii() { let content = r#"line0 line1"#; - let lines = StringOffsets::new(content); + let lines: StringOffsets = StringOffsets::new(content); assert_eq!(lines.utf8_to_char_pos(0), pos(0, 0)); assert_eq!(lines.utf8_to_char_pos(1), pos(0, 1)); assert_eq!(lines.utf8_to_char_pos(6), pos(1, 0)); @@ -504,7 +549,7 @@ line1"#; let content = r#"❤️ line0 line1 ✅ line2"#; - let lines = StringOffsets::new(content); + let lines: StringOffsets = StringOffsets::new(content); assert_eq!(lines.utf8_to_char_pos(0), pos(0, 0)); // ❤️ takes 6 bytes to represent in utf8 (2 code points) assert_eq!(lines.utf8_to_char_pos(1), pos(0, 0)); assert_eq!(lines.utf8_to_char_pos(2), pos(0, 0)); @@ -535,7 +580,7 @@ line1 fn test_small() { // Á - 2 bytes utf8 let content = r#"❤️ line0 ❤️Á 👋"#; - let lines = StringOffsets::new(content); + let lines: StringOffsets = StringOffsets::new(content); let mut utf16_index = 0; let mut char_index = 0; for (byte_index, char) in content.char_indices() { @@ -555,7 +600,7 @@ line1 // ^~~~ utf8: 1 char, 1 byte, utf16: 1 code unit // ^~~~~ utf8: 1 char, 2 bytes, utf16: 1 code unit // ^~~~~~ utf8: 2 chars, 3 byte ea., utf16: 2 code units - let lines = StringOffsets::new(content); + let lines: StringOffsets = StringOffsets::new(content); // UTF-16 positions assert_eq!(lines.utf8_to_utf16_pos(0), pos(0, 0)); // ❤️ @@ -601,7 +646,7 @@ line1 #[test] fn test_critical_input_len() { let content = [b'a'; 16384]; - let lines = StringOffsets::from_bytes(&content); + let lines: StringOffsets = StringOffsets::from_bytes(&content); assert_eq!(lines.utf8_to_utf16_pos(16384), pos(1, 0)); } } diff --git a/crates/string-offsets/src/wasm.rs b/crates/string-offsets/src/wasm.rs new file mode 100644 index 0000000..a663865 --- /dev/null +++ b/crates/string-offsets/src/wasm.rs @@ -0,0 +1,77 @@ +use wasm_bindgen::prelude::*; + +use crate::{AllConfig, Pos, StringOffsets as StringOffsetsImpl}; + +#[wasm_bindgen] +pub struct StringOffsets(StringOffsetsImpl); + +#[wasm_bindgen] +#[allow(non_snake_case)] +impl StringOffsets { + #[wasm_bindgen(constructor)] + pub fn new(content: &str) -> Self { + Self(StringOffsetsImpl::new(content)) + } + + #[allow(unused_variables)] + #[wasm_bindgen(static_method_of = StringOffsets)] + pub fn from_bytes(content: &[u8]) -> Self { + Self(StringOffsetsImpl::from_bytes(content)) + } + + pub fn lines(&self) -> usize { + self.0.lines() + } + + pub fn lineToUtf8Begin(&self, line_number: usize) -> usize { + self.0.line_to_utf8_begin(line_number) + } + + pub fn lineToUtf8End(&self, line_number: usize) -> usize { + self.0.line_to_utf8_end(line_number) + } + + pub fn utf8ToLine(&self, byte_number: usize) -> usize { + self.0.utf8_to_line(byte_number) + } + + pub fn lineChars(&self, line_number: usize) -> usize { + self.0.line_chars(line_number) + } + + pub fn lineToCharBegin(&self, line_number: usize) -> usize { + self.0.line_to_char_begin(line_number) + } + + pub fn lineToCharEnd(&self, line_number: usize) -> usize { + self.0.line_to_char_end(line_number) + } + + pub fn utf8ToCharPos(&self, byte_number: usize) -> Pos { + self.0.utf8_to_char_pos(byte_number) + } + + pub fn utf8ToChar(&self, byte_number: usize) -> usize { + self.0.utf8_to_char(byte_number) + } + + pub fn charToUtf8(&self, char_number: usize) -> usize { + self.0.char_to_utf8(char_number) + } + + pub fn utf8ToUtf16(&self, byte_number: usize) -> usize { + self.0.utf8_to_utf16(byte_number) + } + + pub fn lineToUtf16Begin(&self, line_number: usize) -> usize { + self.0.line_to_utf16_begin(line_number) + } + + pub fn lineToUtf16End(&self, line_number: usize) -> usize { + self.0.line_to_utf16_end(line_number) + } + + pub fn utf8ToUtf16Pos(&self, byte_number: usize) -> Pos { + self.0.utf8_to_utf16_pos(byte_number) + } +}