├── .gitignore ├── Cargo.toml ├── CHANGELOG.md ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── upload_binaries.yml ├── LICENSE ├── README.md ├── src ├── main.rs └── sampling.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "randline" 3 | version = "1.0.1" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | assert_cmd = "2" 8 | rand = "0.9" 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.0.1 - 2025-01-13 4 | 5 | Internal refactoring. 6 | 7 | This fixes a theoretically possible but statistically unlikely bug where `randline` could return less than *k* items, even when there were less than *k* items in the input. 8 | 9 | ## v1.0.0 - 2025-01-11 10 | 11 | Initial release. 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "09:00" 9 | - package-ecosystem: "cargo" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | day: "monday" 14 | time: "09:00" 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | name: build 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: dtolnay/rust-toolchain@v1 15 | with: 16 | toolchain: stable 17 | 18 | - run: cargo build 19 | - run: cargo test 20 | - run: cargo fmt --check 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/workflows/upload_binaries.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.* 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: taiki-e/create-gh-release-action@v1 16 | with: 17 | changelog: CHANGELOG.md 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | upload-assets: 22 | strategy: 23 | matrix: 24 | include: 25 | - target: aarch64-unknown-linux-gnu 26 | - target: aarch64-unknown-linux-musl 27 | - target: x86_64-unknown-linux-gnu 28 | - target: x86_64-unknown-linux-musl 29 | 30 | - target: aarch64-apple-darwin 31 | os: macos-latest 32 | - target: x86_64-apple-darwin 33 | os: macos-latest 34 | 35 | - target: x86_64-pc-windows-msvc 36 | os: windows-latest 37 | 38 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 39 | permissions: 40 | contents: write 41 | steps: 42 | - uses: actions/checkout@v6 43 | - uses: taiki-e/upload-rust-binary-action@v1 44 | with: 45 | target: ${{ matrix.target }} 46 | bin: randline 47 | tar: all 48 | zip: windows 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # randline 2 | 3 | This tool picks one or more random lines from a file (or anything else you pass to it). 4 | 5 | ```console 6 | $ randline < /usr/share/dict/words 7 | Urania 8 | 9 | $ randline 3 < /usr/share/dict/words 10 | foolhardiness 11 | rhinoscopic 12 | wormhood 13 | ``` 14 | 15 | 16 | 17 | 18 | ## How it works 19 | 20 | This tool uses [reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling) to select the random lines; in particular Algorithm L. 21 | 22 | I wrote it as a way to understand how reservoir sampling works, and to try using Rust generics. 23 | Although the final tool only deals with strings, the underlying `reservoir_sample` can sample iterators of any type. 24 | 25 | 26 | 27 | 28 | 29 | ## Installation 30 | 31 | You can download compiled binaries from the [GitHub releases](https://github.com/alexwlchan/randline/releases). 32 | 33 | Alternatively, you can install from source. 34 | You need Rust installed; I recommend using [Rustup]. 35 | Then clone this repository and compile the code: 36 | 37 | ```console 38 | $ git clone "https://github.com/alexwlchan/randline.git" 39 | $ cd randline 40 | $ cargo install --path . 41 | ``` 42 | 43 | [Rustup]: https://rustup.rs/ 44 | 45 | 46 | 47 | 48 | 49 | ## Usage 50 | 51 | You need to pipe input to `randline`. 52 | If you don't pass an argument, it will print a single random line. 53 | 54 | ```console 55 | $ randline < /usr/share/dict/words 56 | blithen 57 | ``` 58 | 59 | You can choose the number of random lines to print by passing a single argument `k`: 60 | 61 | ```console 62 | $ randline 3 < /usr/share/dict/words 63 | unprofessed 64 | ragout 65 | Tarpeia 66 | ``` 67 | 68 | You can also pipe the output of another command to it, for example if I wanted to find 5 random words starting with 'a': 69 | 70 | ```console 71 | $ grep '^a' /usr/share/dict/words | randline 5 72 | approachabl 73 | autecological 74 | alogical 75 | ambrain 76 | anticonstitutionally 77 | ``` 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ## License 86 | 87 | MIT. 88 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use std::io::BufRead; 4 | use std::iter::Iterator; 5 | 6 | mod sampling; 7 | 8 | fn main() { 9 | // Read the user's command line arguments (if any) 10 | // 11 | // 0 arguments = get a single random line 12 | // 1 argument k = get that number of lines 13 | // >1 arguments = error 14 | // 15 | let args: Vec<_> = std::env::args().collect(); 16 | 17 | let k = match args.len() { 18 | 1 => 1, 19 | 2 => match args[1].parse::() { 20 | Ok(parsed_k) if parsed_k > 0 => parsed_k, 21 | _ => { 22 | eprintln!("Usage: randline [k]"); 23 | std::process::exit(1) 24 | } 25 | }, 26 | _ => { 27 | eprintln!("Usage: randline [k]"); 28 | std::process::exit(1) 29 | } 30 | }; 31 | 32 | let lines = std::io::stdin().lock().lines().map(|line| match line { 33 | Ok(ln) => ln, 34 | Err(e) => { 35 | eprintln!("Unable to read from stdin: {:?}", e); 36 | std::process::exit(1) 37 | } 38 | }); 39 | 40 | let sample = sampling::reservoir_sample(lines, k); 41 | 42 | for line in sample { 43 | println!("{}", line); 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod cli_tests { 49 | use assert_cmd::Command; 50 | 51 | // Note: for the purposes of the CLI tests, I trust that the reservoir 52 | // sampling code works correctly -- that's tested separately. I'm just 53 | // checking the CLI parses the options correctly, so all the input lines 54 | // are the same for easy assertions. 55 | 56 | // If you call `randline` without any arguments, it picks a single line. 57 | #[test] 58 | fn it_selects_a_single_line_if_no_arg() { 59 | Command::cargo_bin("randline") 60 | .unwrap() 61 | .write_stdin("a\na\na\na\na\na\n") 62 | .assert() 63 | .success() 64 | .stdout("a\n") 65 | .stderr(""); 66 | } 67 | 68 | // If you pass an argument `k` and there are more lines than `k`, 69 | // it selects a subset of them. 70 | #[test] 71 | fn it_selects_k_lines_if_more_lines_than_k() { 72 | Command::cargo_bin("randline") 73 | .unwrap() 74 | .arg("2") 75 | .write_stdin("a\na\na\na\na\na\n") 76 | .assert() 77 | .success() 78 | .stdout("a\na\n") 79 | .stderr(""); 80 | } 81 | 82 | // If you pass an argument `k` and there are that number of lines, 83 | // it selects all of them. 84 | #[test] 85 | fn it_selects_k_lines_if_equal_lines_to_k() { 86 | Command::cargo_bin("randline") 87 | .unwrap() 88 | .arg("2") 89 | .write_stdin("a\na\n") 90 | .assert() 91 | .success() 92 | .stdout("a\na\n") 93 | .stderr(""); 94 | } 95 | 96 | // If you pass an argument `k` and there are less lines than `k`, 97 | // it selects all of them. 98 | #[test] 99 | fn it_selects_k_lines_if_less_lines_than_k() { 100 | Command::cargo_bin("randline") 101 | .unwrap() 102 | .arg("5") 103 | .write_stdin("a\na\n") 104 | .assert() 105 | .success() 106 | .stdout("a\na\n") 107 | .stderr(""); 108 | } 109 | 110 | // Passing a non-integer argument is an error. 111 | #[test] 112 | fn it_fails_if_non_integer_argument() { 113 | Command::cargo_bin("randline") 114 | .unwrap() 115 | .arg("XXX") 116 | .assert() 117 | .failure() 118 | .code(1) 119 | .stdout("") 120 | .stderr("Usage: randline [k]\n"); 121 | } 122 | 123 | // Passing k=0 is an error. 124 | #[test] 125 | fn it_fails_if_k_equals_zero() { 126 | Command::cargo_bin("randline") 127 | .unwrap() 128 | .arg("0") 129 | .assert() 130 | .failure() 131 | .code(1) 132 | .stdout("") 133 | .stderr("Usage: randline [k]\n"); 134 | } 135 | 136 | // Passing k<0 is an error. 137 | #[test] 138 | fn it_fails_if_k_negative() { 139 | Command::cargo_bin("randline") 140 | .unwrap() 141 | .arg("-1") 142 | .assert() 143 | .failure() 144 | .code(1) 145 | .stdout("") 146 | .stderr("Usage: randline [k]\n"); 147 | } 148 | 149 | // Passing more than one argument is an error. 150 | #[test] 151 | fn it_fails_if_too_many_args() { 152 | Command::cargo_bin("randline") 153 | .unwrap() 154 | .args(&["1", "2", "3"]) 155 | .assert() 156 | .failure() 157 | .code(1) 158 | .stdout("") 159 | .stderr("Usage: randline [k]\n"); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/sampling.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use std::cmp::Ordering; 3 | use std::collections::BinaryHeap; 4 | use std::ptr; 5 | 6 | struct WeightedItem { 7 | item: T, 8 | weight: f64, 9 | } 10 | 11 | // Two items are only equal if they are identical -- that is, they're 12 | // the same underlying object in memory. 13 | // 14 | // [I suppose it's theoretically possible that there could be duplicate 15 | // reservoir entries, if the RNG was bugged and the input has repeated 16 | // values -- seems unlikely in practice, but this protects against it 17 | // just in case.] 18 | impl PartialEq for WeightedItem { 19 | fn eq(&self, other: &Self) -> bool { 20 | ptr::eq(self, other) 21 | } 22 | } 23 | 24 | impl Eq for WeightedItem {} 25 | 26 | // Rust doesn't implement ordering for f64 because it includes NaN 27 | // which makes everything a mess. In particular NaN isn't comparable 28 | // with other floating-point numbers. 29 | // 30 | // We're generating all the f64 weights we'll be dealing with, so we 31 | // know we'll never have NaN in the mix -- we can do a partial comparison 32 | // and assert the two values are comparable when we unwrap. 33 | impl PartialOrd for WeightedItem { 34 | fn partial_cmp(&self, other: &Self) -> Option { 35 | Some(self.cmp(other)) 36 | } 37 | } 38 | 39 | impl Ord for WeightedItem { 40 | fn cmp(&self, other: &Self) -> Ordering { 41 | self.weight.partial_cmp(&other.weight).unwrap() 42 | } 43 | } 44 | 45 | /// Choose a sample of `k` items from the iterator `items. 46 | /// 47 | /// Each item has an equal chance of being picked -- that is, there's 48 | /// a 1/N chance of choosing an item, where N is the length of the iterator. 49 | /// 50 | /// This implements "Algorithm L" for reservoir sampling, as described 51 | /// on the Wikipedia page: 52 | /// https://en.wikipedia.org/wiki/Reservoir_sampling#Optimal:_Algorithm_L 53 | /// 54 | pub fn reservoir_sample(mut items: impl Iterator, k: usize) -> Vec { 55 | // Taking a sample with k=0 doesn't make much sense in practice, 56 | // but we include this to avoid problems downstream. 57 | if k == 0 { 58 | return vec![]; 59 | } 60 | 61 | // Create an empty reservoir. 62 | let mut reservoir: BinaryHeap> = BinaryHeap::with_capacity(k); 63 | 64 | // Fill the reservoir with the first k items. If there are less 65 | // than n items, we can exit immediately. 66 | for _ in 1..=k { 67 | match items.next() { 68 | Some(this_item) => reservoir.push(WeightedItem { 69 | item: this_item, 70 | weight: pick_weight(), 71 | }), 72 | None => return reservoir.into_vec().into_iter().map(|r| r.item).collect(), 73 | }; 74 | } 75 | 76 | // What's the largest weight seen so far? 77 | // 78 | // Note: we're okay to `unwrap()` here because we know that `reservoir` 79 | // contains at least one item. Either `items` was non-empty, or if itwas 80 | // was empty, then we'd already have returned when trying to fill the 81 | // reservoir with the first k items. 82 | let mut max_weight: f64 = reservoir.peek().unwrap().weight; 83 | 84 | // Now go through the remaining items. 85 | for this_item in items { 86 | // Choose a weight for this item. 87 | let this_weight = pick_weight(); 88 | 89 | // If this is greater than the weights seen so far, we can ignore 90 | // this item and move on to the next one. 91 | if this_weight > max_weight { 92 | continue; 93 | } 94 | 95 | // Otherwise, this item has a lower weight than the current item 96 | // with max weight -- so we'll replace that item. 97 | assert!(reservoir.pop().is_some()); 98 | reservoir.push(WeightedItem { 99 | item: this_item, 100 | weight: this_weight, 101 | }); 102 | 103 | // Recalculate the max weight for the new sample. 104 | max_weight = reservoir.peek().unwrap().weight; 105 | } 106 | 107 | let sample: Vec = reservoir.into_vec().into_iter().map(|r| r.item).collect(); 108 | assert!(sample.len() == k); 109 | sample 110 | } 111 | 112 | /// Create a random weight u_i ~ U[0,1] 113 | fn pick_weight() -> f64 { 114 | rand::rng().random_range(0.0..1.0) 115 | } 116 | 117 | #[cfg(test)] 118 | mod reservoir_sample_tests { 119 | use super::*; 120 | use std::collections::HashMap; 121 | 122 | // If there are no items, then the sample is empty. 123 | #[test] 124 | fn it_returns_an_empty_sample_for_an_empty_input() { 125 | let items: Vec = vec![]; 126 | let sample = reservoir_sample(items.into_iter(), 5); 127 | 128 | assert_eq!(sample.len(), 0); 129 | } 130 | 131 | // If there are less items than the sample size, then the sample is 132 | // the complete set. 133 | #[test] 134 | fn it_returns_complete_sample_if_less_items_than_sample_size() { 135 | let items = vec!["a", "b", "c"]; 136 | let sample = reservoir_sample(items.into_iter(), 5); 137 | 138 | assert!(equivalent_items(sample, vec!["a", "b", "c"])); 139 | } 140 | 141 | // If there's an equal number of items to the sample size, then the 142 | // sample is the complete set. 143 | #[test] 144 | fn it_returns_complete_sample_if_item_count_equal_to_sample_size() { 145 | let items = vec!["a", "b", "c"]; 146 | let sample = reservoir_sample(items.into_iter(), 3); 147 | 148 | assert!(equivalent_items(sample, vec!["a", "b", "c"])); 149 | } 150 | 151 | // If k=0, then it returns an empty sample. 152 | #[test] 153 | fn it_returns_an_empty_sample_if_k_zero() { 154 | let items = vec!["a", "b", "c"]; 155 | let sample = reservoir_sample(items.into_iter(), 0); 156 | 157 | assert_eq!(sample.len(), 0); 158 | } 159 | 160 | // It chooses items with a uniform distribution -- every item has 161 | // an equal chance of being picked. 162 | // 163 | // We take a large number of samples of the integers 0..n, and check 164 | // that each integer is picked about as many times as we expect. 165 | #[test] 166 | fn test_distribution() { 167 | let k = 20; 168 | let n = 100; 169 | let iterations = 10000; 170 | 171 | // How often was each integer picked? 172 | let mut counts: HashMap = HashMap::new(); 173 | 174 | // Run many iterations, create a sample, and record how many 175 | // times each integer was picked. 176 | for _ in 0..iterations { 177 | let items = 0..n; 178 | let sample = reservoir_sample(items, k); 179 | 180 | for s in sample.into_iter() { 181 | *counts.entry(s).or_insert(0) += 1; 182 | } 183 | } 184 | 185 | // Now check that each number appears roughly as many times 186 | // as we'd expect (within reasonable bounds). 187 | let total_samples = iterations * k; 188 | let expected = total_samples as f64 / n as f64; 189 | 190 | for item in 0..n { 191 | let item_count = *counts.get(&item).unwrap_or(&0); 192 | 193 | let ratio = (item_count as f64) / expected; 194 | assert!( 195 | ratio > 0.8 && ratio < 1.2, 196 | "Distribution appears skewed: count={}, expected={}", 197 | item_count, 198 | expected 199 | ); 200 | } 201 | } 202 | 203 | /// Returns true if two vectors contain the same items (but potentially 204 | /// in a different order), false otherwise. 205 | /// 206 | /// equivalent_items(vec![1, 3, 2], vec![3, 2, 1]) 207 | /// => true 208 | /// 209 | /// equivalent_items(vec![4, 5, 6], vec![3, 2, 1]) 210 | /// => false 211 | /// 212 | fn equivalent_items( 213 | mut vec1: Vec, 214 | mut vec2: Vec, 215 | ) -> bool { 216 | vec1.sort(); 217 | vec2.sort(); 218 | 219 | vec1 == vec2 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.10" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 10 | 11 | [[package]] 12 | name = "assert_cmd" 13 | version = "2.0.17" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" 16 | dependencies = [ 17 | "anstyle", 18 | "bstr", 19 | "doc-comment", 20 | "libc", 21 | "predicates", 22 | "predicates-core", 23 | "predicates-tree", 24 | "wait-timeout", 25 | ] 26 | 27 | [[package]] 28 | name = "bitflags" 29 | version = "2.8.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 32 | 33 | [[package]] 34 | name = "bstr" 35 | version = "1.11.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 38 | dependencies = [ 39 | "memchr", 40 | "regex-automata", 41 | "serde", 42 | ] 43 | 44 | [[package]] 45 | name = "byteorder" 46 | version = "1.5.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 49 | 50 | [[package]] 51 | name = "cfg-if" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 55 | 56 | [[package]] 57 | name = "difflib" 58 | version = "0.4.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 61 | 62 | [[package]] 63 | name = "doc-comment" 64 | version = "0.3.3" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 67 | 68 | [[package]] 69 | name = "getrandom" 70 | version = "0.3.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 73 | dependencies = [ 74 | "cfg-if", 75 | "libc", 76 | "wasi", 77 | "windows-targets", 78 | ] 79 | 80 | [[package]] 81 | name = "libc" 82 | version = "0.2.169" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 85 | 86 | [[package]] 87 | name = "memchr" 88 | version = "2.7.4" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 91 | 92 | [[package]] 93 | name = "ppv-lite86" 94 | version = "0.2.20" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 97 | dependencies = [ 98 | "zerocopy 0.7.35", 99 | ] 100 | 101 | [[package]] 102 | name = "predicates" 103 | version = "3.1.3" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 106 | dependencies = [ 107 | "anstyle", 108 | "difflib", 109 | "predicates-core", 110 | ] 111 | 112 | [[package]] 113 | name = "predicates-core" 114 | version = "1.0.9" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 117 | 118 | [[package]] 119 | name = "predicates-tree" 120 | version = "1.0.12" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 123 | dependencies = [ 124 | "predicates-core", 125 | "termtree", 126 | ] 127 | 128 | [[package]] 129 | name = "proc-macro2" 130 | version = "1.0.92" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 133 | dependencies = [ 134 | "unicode-ident", 135 | ] 136 | 137 | [[package]] 138 | name = "quote" 139 | version = "1.0.38" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 142 | dependencies = [ 143 | "proc-macro2", 144 | ] 145 | 146 | [[package]] 147 | name = "rand" 148 | version = "0.9.2" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 151 | dependencies = [ 152 | "rand_chacha", 153 | "rand_core", 154 | ] 155 | 156 | [[package]] 157 | name = "rand_chacha" 158 | version = "0.9.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 161 | dependencies = [ 162 | "ppv-lite86", 163 | "rand_core", 164 | ] 165 | 166 | [[package]] 167 | name = "rand_core" 168 | version = "0.9.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" 171 | dependencies = [ 172 | "getrandom", 173 | "zerocopy 0.8.14", 174 | ] 175 | 176 | [[package]] 177 | name = "randline" 178 | version = "1.0.1" 179 | dependencies = [ 180 | "assert_cmd", 181 | "rand", 182 | ] 183 | 184 | [[package]] 185 | name = "regex-automata" 186 | version = "0.4.9" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 189 | 190 | [[package]] 191 | name = "serde" 192 | version = "1.0.217" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 195 | dependencies = [ 196 | "serde_derive", 197 | ] 198 | 199 | [[package]] 200 | name = "serde_derive" 201 | version = "1.0.217" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 204 | dependencies = [ 205 | "proc-macro2", 206 | "quote", 207 | "syn", 208 | ] 209 | 210 | [[package]] 211 | name = "syn" 212 | version = "2.0.95" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" 215 | dependencies = [ 216 | "proc-macro2", 217 | "quote", 218 | "unicode-ident", 219 | ] 220 | 221 | [[package]] 222 | name = "termtree" 223 | version = "0.5.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 226 | 227 | [[package]] 228 | name = "unicode-ident" 229 | version = "1.0.14" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 232 | 233 | [[package]] 234 | name = "wait-timeout" 235 | version = "0.2.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 238 | dependencies = [ 239 | "libc", 240 | ] 241 | 242 | [[package]] 243 | name = "wasi" 244 | version = "0.13.3+wasi-0.2.2" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 247 | dependencies = [ 248 | "wit-bindgen-rt", 249 | ] 250 | 251 | [[package]] 252 | name = "windows-targets" 253 | version = "0.52.6" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 256 | dependencies = [ 257 | "windows_aarch64_gnullvm", 258 | "windows_aarch64_msvc", 259 | "windows_i686_gnu", 260 | "windows_i686_gnullvm", 261 | "windows_i686_msvc", 262 | "windows_x86_64_gnu", 263 | "windows_x86_64_gnullvm", 264 | "windows_x86_64_msvc", 265 | ] 266 | 267 | [[package]] 268 | name = "windows_aarch64_gnullvm" 269 | version = "0.52.6" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 272 | 273 | [[package]] 274 | name = "windows_aarch64_msvc" 275 | version = "0.52.6" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 278 | 279 | [[package]] 280 | name = "windows_i686_gnu" 281 | version = "0.52.6" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 284 | 285 | [[package]] 286 | name = "windows_i686_gnullvm" 287 | version = "0.52.6" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 290 | 291 | [[package]] 292 | name = "windows_i686_msvc" 293 | version = "0.52.6" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 296 | 297 | [[package]] 298 | name = "windows_x86_64_gnu" 299 | version = "0.52.6" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 302 | 303 | [[package]] 304 | name = "windows_x86_64_gnullvm" 305 | version = "0.52.6" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 308 | 309 | [[package]] 310 | name = "windows_x86_64_msvc" 311 | version = "0.52.6" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 314 | 315 | [[package]] 316 | name = "wit-bindgen-rt" 317 | version = "0.33.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 320 | dependencies = [ 321 | "bitflags", 322 | ] 323 | 324 | [[package]] 325 | name = "zerocopy" 326 | version = "0.7.35" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 329 | dependencies = [ 330 | "byteorder", 331 | "zerocopy-derive 0.7.35", 332 | ] 333 | 334 | [[package]] 335 | name = "zerocopy" 336 | version = "0.8.14" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" 339 | dependencies = [ 340 | "zerocopy-derive 0.8.14", 341 | ] 342 | 343 | [[package]] 344 | name = "zerocopy-derive" 345 | version = "0.7.35" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 348 | dependencies = [ 349 | "proc-macro2", 350 | "quote", 351 | "syn", 352 | ] 353 | 354 | [[package]] 355 | name = "zerocopy-derive" 356 | version = "0.8.14" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" 359 | dependencies = [ 360 | "proc-macro2", 361 | "quote", 362 | "syn", 363 | ] 364 | --------------------------------------------------------------------------------