├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── release_rust.yml │ ├── build.yml │ └── release_python.yml ├── Cargo.toml ├── LICENSE.txt ├── pyproject.toml ├── src ├── encodings.rs ├── lib.rs ├── main.rs ├── strings_writer.rs ├── strings_extractor.rs ├── python_bindings.rs └── strings.rs ├── rust_strings.pyi ├── tests ├── test_python_bindings.py └── test_lib.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | /venv 4 | __pycache__ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/release_rust.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | tags: 5 | - "v**" 6 | 7 | name: Rust release 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | 19 | - uses: actions-rs/cargo@v1 20 | name: login 21 | with: 22 | command: login 23 | args: ${{ secrets.CRATES_TOKEN }} 24 | 25 | - uses: actions-rs/cargo@v1 26 | name: publish 27 | with: 28 | command: publish 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-strings" 3 | version = "0.6.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["iddohau@gmail.com"] 7 | description = "`rust-strings` is a library to extract ascii strings from binary data" 8 | readme = "README.md" 9 | documentation = "https://docs.rs/rust-strings" 10 | repository = "https://github.com/iddohau/rust-strings" 11 | homepage = "https://github.com/iddohau/rust-strings" 12 | keywords = ["strings", "encoding"] 13 | categories = ["encoding"] 14 | include = ["/LICENSE.txt", "/README.md", "/src", "/pyproject.toml"] 15 | 16 | [lib] 17 | name = "rust_strings" 18 | path = "src/lib.rs" 19 | crate-type = ["cdylib", "rlib"] 20 | 21 | [[bin]] 22 | name = "rust-strings" 23 | path = "src/main.rs" 24 | required-features = ["cli"] 25 | 26 | [dependencies] 27 | clap = { version = "4.5.8", features = ["derive"], optional = true } 28 | pyo3 = { version = "0.21.2", features = ["extension-module", "gil-refs"], optional = true } 29 | 30 | [dev-dependencies] 31 | tempfile = "3.10" 32 | 33 | [features] 34 | python_bindings = ["pyo3"] 35 | cli = ["clap"] 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Iddo Hauschner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin==1.5.1", "cffi"] 3 | build-backend = "maturin" 4 | 5 | 6 | [project] 7 | name = "rust-strings" 8 | version = "0.6.0" 9 | description = "Extract strings from binary data" 10 | authors = [ 11 | {email = "iddohau@gmail.com", name = "Iddo Hauschner"}, 12 | ] 13 | maintainers = [ 14 | {email = "iddohau@gmail.com", name = "Iddo Hauschner"}, 15 | ] 16 | license = {file = "LICENSE.txt"} 17 | readme = "README.md" 18 | requires-python = ">=3.7" 19 | homepage = "https://github.com/iddohau/rust-strings" 20 | repository = "https://github.com/iddohau/rust-strings" 21 | documentation = "https://github.com/iddohau/rust-strings" 22 | 23 | keywords = ["strings"] 24 | 25 | classifiers = [ 26 | "Topic :: Software Development", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: Implementation :: CPython", 34 | "Programming Language :: Rust", 35 | ] 36 | 37 | [dev-dependencies] 38 | pytest = "^7.2.0" 39 | 40 | [tool.maturin] 41 | profile = "release" 42 | features = ["python_bindings"] -------------------------------------------------------------------------------- /src/encodings.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Copy, Clone)] 6 | pub enum Encoding { 7 | ASCII, 8 | UTF16LE, 9 | UTF16BE, 10 | } 11 | 12 | impl fmt::Display for Encoding { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | write!(f, "{:?}", self) 15 | } 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct EncodingNotFoundError { 20 | encoding: String, 21 | } 22 | 23 | impl fmt::Display for EncodingNotFoundError { 24 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 25 | write!(f, "Encoding not found: {:?}", self.encoding) 26 | } 27 | } 28 | 29 | impl EncodingNotFoundError { 30 | fn new(encoding: String) -> Self { 31 | EncodingNotFoundError { encoding } 32 | } 33 | } 34 | 35 | impl Error for EncodingNotFoundError {} 36 | 37 | impl FromStr for Encoding { 38 | type Err = EncodingNotFoundError; 39 | 40 | fn from_str(encoding: &str) -> Result { 41 | let encoding: &str = &encoding.to_lowercase(); 42 | match encoding { 43 | "utf-16le" => Ok(Encoding::UTF16LE), 44 | "utf-16be" => Ok(Encoding::UTF16BE), 45 | "ascii" => Ok(Encoding::ASCII), 46 | "utf8" => Ok(Encoding::ASCII), 47 | "utf-8" => Ok(Encoding::ASCII), 48 | _ => Err(EncodingNotFoundError::new(encoding.to_owned())), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: Lint & Test 4 | 5 | env: 6 | name: rust-strings 7 | 8 | jobs: 9 | build-rust: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | components: rustfmt, clippy 18 | 19 | - uses: actions-rs/cargo@v1 20 | name: build 21 | with: 22 | command: build 23 | 24 | - uses: actions-rs/cargo@v1 25 | name: test 26 | with: 27 | command: test 28 | 29 | - uses: actions-rs/cargo@v1 30 | name: fmt 31 | with: 32 | command: fmt 33 | args: --all -- --check 34 | 35 | - uses: actions-rs/cargo@v1 36 | name: clippy 37 | with: 38 | command: clippy 39 | args: -- -D warnings 40 | 41 | build-python: 42 | runs-on: ubuntu-latest 43 | strategy: 44 | matrix: 45 | target: [x86_64] 46 | python_version: ["3.12"] 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-python@v5 50 | with: 51 | python-version: ${{ matrix.python_version }} 52 | architecture: x64 53 | - name: Build Wheels 54 | uses: PyO3/maturin-action@v1 55 | with: 56 | rust-toolchain: stable 57 | target: ${{ matrix.target }} 58 | manylinux: auto 59 | args: --release -i python${{ matrix.python_version }} --features python_bindings --out dist 60 | - name: Install built wheel 61 | if: matrix.target == 'x86_64' 62 | run: | 63 | pip install ${{ env.name }} --no-index --find-links dist --force-reinstall 64 | pip install pytest 65 | pytest -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Rust Strings 2 | //! 3 | //! `rust-strings` is a library to extract ascii strings from binary data. 4 | //! It is similar to the command `strings`. 5 | //! 6 | //! ## Examples: 7 | //! ``` 8 | //! use rust_strings::{FileConfig, BytesConfig, strings, dump_strings, Encoding}; 9 | //! use std::path::{Path, PathBuf}; 10 | //! 11 | //! let config = FileConfig::new(Path::new("/bin/ls")).with_min_length(5); 12 | //! let extracted_strings = strings(&config); 13 | //! 14 | //! // Extract utf16le strings 15 | //! let config = FileConfig::new(Path::new("C:\\Windows\\notepad.exe")) 16 | //! .with_min_length(15) 17 | //! .with_encoding(Encoding::UTF16LE); 18 | //! let extracted_strings = strings(&config); 19 | //! 20 | //! // Extract ascii and utf16le strings 21 | //! let config = FileConfig::new(Path::new("C:\\Windows\\notepad.exe")) 22 | //! .with_min_length(15) 23 | //! .with_encoding(Encoding::ASCII) 24 | //! .with_encoding(Encoding::UTF16LE); 25 | //! let extracted_strings = strings(&config); 26 | //! 27 | //! let config = BytesConfig::new(b"test\x00".to_vec()); 28 | //! let extracted_strings = strings(&config); 29 | //! assert_eq!(vec![(String::from("test"), 0)], extracted_strings.unwrap()); 30 | //! 31 | //! // Dump strings into `strings.json` file. 32 | //! let config = BytesConfig::new(b"test\x00".to_vec()); 33 | //! dump_strings(&config, PathBuf::from("strings.json")); 34 | //! ``` 35 | 36 | use std::error::Error; 37 | 38 | mod encodings; 39 | mod strings; 40 | mod strings_extractor; 41 | mod strings_writer; 42 | 43 | type ErrorResult = Result<(), Box>; 44 | 45 | pub use encodings::{Encoding, EncodingNotFoundError}; 46 | pub use strings::{dump_strings, strings, BytesConfig, Config, FileConfig, StdinConfig}; 47 | 48 | #[cfg(feature = "python_bindings")] 49 | mod python_bindings; 50 | -------------------------------------------------------------------------------- /rust_strings.pyi: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional, List, Tuple, Union 3 | 4 | 5 | def strings( 6 | file_path: Optional[Union[str, Path]] = None, 7 | bytes: Optional[bytes] = None, 8 | min_length: int = 3, 9 | encodings: List[str] = ["ascii"], 10 | buffer_size: int = 1024 * 1024, 11 | ) -> List[Tuple[str, int]]: 12 | """ 13 | Extract strings from binary file or bytes. 14 | :param file_path: path to file (can't be with bytes option) 15 | :param bytes: bytes (can't be with file_path option) 16 | :param min_length: strings minimum length 17 | :param encodings: strings encodings (default is ["ascii"]) 18 | :param buffer_size: the buffer size to read the file (relevant only to file_path option) 19 | :return: list of tuples of string and offset 20 | :raises: raise StringsException if there is any error during string extraction 21 | raise EncodingNotFoundException if the function got an unsupported encondings 22 | """ 23 | ... 24 | 25 | 26 | def dump_strings( 27 | output_file: Union[str, Path], 28 | file_path: Optional[Union[str, Path]] = None, 29 | bytes: Optional[bytes] = None, 30 | min_length: int = 3, 31 | encodings: List[str] = ["ascii"], 32 | buffer_size: int = 1024 * 1024, 33 | ) -> List[Tuple[str, int]]: 34 | """ 35 | Dump strings from binary file or bytes to json file. 36 | :param output_file: path to file to dump into 37 | :param file_path: path to file (can't be with bytes option) 38 | :param bytes: bytes (can't be with file_path option) 39 | :param min_length: strings minimum length 40 | :param encodings: strings encodings (default is ["ascii"]) 41 | :param buffer_size: the buffer size to read the file (relevant only to file_path option) 42 | :return: list of tuples of string and offset 43 | :raises: raise StringsException if there is any error during string extraction 44 | raise EncodingNotFoundException if the function got an unsupported encondings 45 | """ 46 | ... 47 | -------------------------------------------------------------------------------- /tests/test_python_bindings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | from uuid import uuid4 5 | 6 | import pytest 7 | 8 | import rust_strings 9 | 10 | 11 | @pytest.fixture 12 | def temp_file(tmp_path: Path) -> Path: 13 | file = tmp_path / str(uuid4()) 14 | yield file 15 | os.remove(file) 16 | 17 | 18 | def test_bytes(): 19 | extracted = rust_strings.strings(bytes=b"test\x00") 20 | assert extracted == [("test", 0)] 21 | 22 | 23 | def test_bytes_min_length_1(): 24 | extracted = rust_strings.strings(bytes=b"test\x00", min_length=1) 25 | assert extracted == [("test", 0)] 26 | 27 | 28 | def test_single_byte(): 29 | extracted = rust_strings.strings(bytes=b"t\x00", min_length=1) 30 | assert extracted == [("t", 0)] 31 | 32 | 33 | def test_bytes_with_offset(): 34 | extracted = rust_strings.strings(bytes=b"\x00test") 35 | assert extracted == [("test", 1)] 36 | 37 | 38 | def test_bytes_multiple(): 39 | extracted = rust_strings.strings(bytes=b"\x00test\x00test") 40 | assert extracted == [("test", 1), ("test", 6)] 41 | 42 | 43 | def test_file(temp_file: Path): 44 | temp_file.write_bytes(b"test\x00") 45 | extracted = rust_strings.strings(file_path=temp_file) 46 | assert extracted == [("test", 0)] 47 | 48 | 49 | def test_file_as_str(temp_file: Path): 50 | temp_file.write_bytes(b"test\x00") 51 | extracted = rust_strings.strings(file_path=str(temp_file)) 52 | assert extracted == [("test", 0)] 53 | 54 | 55 | def test_multiple_encodings(): 56 | extracted = rust_strings.strings( 57 | bytes=b"ascii\x01t\x00e\x00s\x00t\x00\x00\x00", encodings=["ascii", "utf-16le"] 58 | ) 59 | assert extracted == [("ascii", 0), ("test", 6)] 60 | 61 | 62 | def test_json_dump(temp_file: Path): 63 | rust_strings.dump_strings(temp_file, bytes=b'\x00\x00test"\n\tmore\x00\x00') 64 | assert json.loads(temp_file.read_text()) == [['test"\n\tmore', 2]] 65 | 66 | 67 | def test_json_dump_multiple_strings(temp_file: Path): 68 | rust_strings.dump_strings( 69 | temp_file, bytes=b'\x00\x00test"\n\tmore\x00\x00more text over here' 70 | ) 71 | assert json.loads(temp_file.read_text()) == [ 72 | ['test"\n\tmore', 2], 73 | ["more text over here", 15], 74 | ] 75 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use rust_strings::{strings, Encoding, FileConfig, StdinConfig}; 3 | use std::path::Path; 4 | use std::process::exit; 5 | use std::str::FromStr; 6 | 7 | #[derive(Parser, Debug)] 8 | #[clap(version = "1.0", author = "Iddo Hauschner", name = "rust-strings")] 9 | struct Opts { 10 | /// file path to run strings on, use "-" for stdin 11 | #[clap(name = "FILE_PATH_ARG")] 12 | file_path_arg: Option, 13 | /// file path to run strings on, use "-" for stdin 14 | #[clap(short, long, name = "FILE_PATH")] 15 | file_path_flag: Option, 16 | /// min length of string 17 | #[clap(short, long, default_value = "3")] 18 | min_length: usize, 19 | /// encoding of string 20 | #[clap(short, long, default_value = "ascii")] 21 | encoding: String, 22 | #[clap(short, long)] 23 | offset: bool, 24 | } 25 | 26 | fn get_file_path(options: &Opts) -> String { 27 | if matches!(options.file_path_arg, Some(_)) && matches!(options.file_path_flag, Some(_)) { 28 | eprintln!("You can't specify file path as argument and as flag together"); 29 | exit(1); 30 | } 31 | let mut file_path = String::new(); 32 | if let Some(file_path_arg) = &options.file_path_arg { 33 | file_path = file_path_arg.clone() 34 | } 35 | if let Some(file_path_flag) = &options.file_path_flag { 36 | file_path = file_path_flag.clone() 37 | } 38 | file_path 39 | } 40 | 41 | fn main() { 42 | let options = Opts::parse(); 43 | let encoding = match Encoding::from_str(&options.encoding) { 44 | Ok(encoding) => encoding, 45 | Err(err) => { 46 | eprintln!("{}", err); 47 | exit(1); 48 | } 49 | }; 50 | let file_path = get_file_path(&options); 51 | let extracted_strings = match file_path == "-" { 52 | true => strings( 53 | &StdinConfig::new() 54 | .with_min_length(options.min_length) 55 | .with_encoding(encoding), 56 | ), 57 | false => { 58 | let path: &Path = Path::new(&file_path); 59 | if !path.is_file() { 60 | eprintln!("File does not exists!"); 61 | exit(1); 62 | } 63 | strings( 64 | &FileConfig::new(path) 65 | .with_min_length(options.min_length) 66 | .with_encoding(encoding), 67 | ) 68 | } 69 | } 70 | .expect("Something went wrong!"); 71 | for (string, offset) in extracted_strings { 72 | if options.offset { 73 | println!("{:10}: {}", offset, string); 74 | } else { 75 | println!("{}", string); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-strings 2 | 3 | [![CI](https://github.com/iddohau/rust-strings/workflows/Rust%20Lint%20%26%20Test/badge.svg?branch=main)](https://github.com/iddohau/rust-strings/actions?query=branch=main) 4 | ![License](https://img.shields.io/github/license/iddohau/rust-strings) 5 | ![Crates.io](https://img.shields.io/crates/v/rust-strings) 6 | [![PyPI](https://img.shields.io/pypi/v/rust-strings.svg)](https://pypi.org/project/rust-strings) 7 | 8 | `rust-strings` is a Rust library for extracting strings from binary data. \ 9 | It also have Python bindings. 10 | 11 | ## Installation 12 | 13 | ### Python 14 | 15 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install `rust-strings`. 16 | 17 | ```bash 18 | pip install rust-strings 19 | ``` 20 | 21 | ### Rust 22 | 23 | `rust-strings` is available on [crates.io](https://crates.io/crates/rust-strings) and can be included in your Cargo enabled project like this: 24 | 25 | ```bash 26 | [dependencies] 27 | rust-strings = "0.6.0" 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Python 33 | 34 | ```python 35 | import rust_strings 36 | 37 | # Get all ascii strings from file with minimun length of string 38 | rust_strings.strings(file_path="/bin/ls", min_length=3) 39 | # [('ELF', 1), 40 | # ('/lib64/ld-linux-x86-64.so.2', 680), 41 | # ('GNU', 720), 42 | # ('., offset: u64) -> ErrorResult; 8 | fn write_char(&mut self, c: char) -> ErrorResult; 9 | fn finish_string_consume(&mut self) -> ErrorResult; 10 | } 11 | 12 | pub struct VectorWriter { 13 | vec: Vec<(String, u64)>, 14 | current_string: String, 15 | current_offset: u64, 16 | } 17 | 18 | impl VectorWriter { 19 | pub fn new() -> Self { 20 | VectorWriter { 21 | vec: vec![], 22 | current_offset: 0, 23 | current_string: String::new(), 24 | } 25 | } 26 | } 27 | 28 | impl StringWriter for VectorWriter { 29 | fn start_string_consume(&mut self, string: Vec, offset: u64) -> ErrorResult { 30 | self.current_offset = offset; 31 | self.current_string = String::with_capacity(string.len()); 32 | string 33 | .into_iter() 34 | .for_each(|c| self.current_string.push(c as char)); 35 | Ok(()) 36 | } 37 | 38 | fn write_char(&mut self, c: char) -> ErrorResult { 39 | self.current_string.push(c); 40 | Ok(()) 41 | } 42 | 43 | fn finish_string_consume(&mut self) -> ErrorResult { 44 | if self.current_string.is_empty() { 45 | return Ok(()); 46 | } 47 | let string = take(&mut self.current_string); 48 | self.vec.push((string, self.current_offset)); 49 | Ok(()) 50 | } 51 | } 52 | 53 | impl VectorWriter { 54 | pub fn get_strings(&mut self) -> Vec<(String, u64)> { 55 | take(&mut self.vec) 56 | } 57 | } 58 | 59 | pub struct JsonWriter { 60 | writer: T, 61 | current_offset: u64, 62 | is_start_writing: bool, 63 | is_first_element: bool, 64 | } 65 | 66 | impl StringWriter for JsonWriter 67 | where 68 | T: Write, 69 | { 70 | fn start_string_consume(&mut self, string: Vec, offset: u64) -> ErrorResult { 71 | self.current_offset = offset; 72 | for ch in string.into_iter() { 73 | self.write_chars_to_writer(ch)?; 74 | } 75 | Ok(()) 76 | } 77 | 78 | fn write_char(&mut self, c: char) -> ErrorResult { 79 | self.write_chars_to_writer(c as u8) 80 | } 81 | 82 | fn finish_string_consume(&mut self) -> ErrorResult { 83 | self.writer.write_all(b"\",")?; 84 | self.writer 85 | .write_all(format!("{}", self.current_offset).as_bytes())?; 86 | self.writer.write_all(b"]")?; 87 | self.is_start_writing = false; 88 | Ok(()) 89 | } 90 | } 91 | 92 | impl JsonWriter 93 | where 94 | T: Write, 95 | { 96 | pub fn new(writer: T) -> Self { 97 | JsonWriter { 98 | writer, 99 | current_offset: 0, 100 | is_start_writing: false, 101 | is_first_element: true, 102 | } 103 | } 104 | 105 | pub fn finish(&mut self) -> ErrorResult { 106 | self.writer.write_all(b"]")?; 107 | Ok(()) 108 | } 109 | 110 | fn write_chars_to_writer(&mut self, c: u8) -> ErrorResult { 111 | if !self.is_start_writing { 112 | self.is_start_writing = true; 113 | if self.is_first_element { 114 | // Start writing the first element, needs to write `[["` 115 | self.writer.write_all(b"[[\"")?; 116 | self.is_first_element = false; 117 | } else { 118 | // Start writing current string, needs to write `,["` 119 | self.writer.write_all(b",[\"")?; 120 | } 121 | } 122 | let v = self.escape_json_character(c); 123 | self.writer.write_all(&v)?; 124 | Ok(()) 125 | } 126 | 127 | fn escape_json_character(&self, c: u8) -> Vec { 128 | match c as char { 129 | '\n' => b"\\n".to_vec(), 130 | '\t' => b"\\t".to_vec(), 131 | '\r' => b"\\r".to_vec(), 132 | '"' => b"\\\"".to_vec(), 133 | '\\' => b"\\\\".to_vec(), 134 | _ => vec![c], 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/test_lib.rs: -------------------------------------------------------------------------------- 1 | use rust_strings::{dump_strings, strings, BytesConfig, Encoding, FileConfig}; 2 | use std::io::{Read, Write}; 3 | use tempfile::NamedTempFile; 4 | 5 | #[test] 6 | fn test_bytes_config() { 7 | let config = BytesConfig::new(vec![116, 101, 115, 116, 0, 0]); 8 | let extracted = strings(&config).unwrap(); 9 | assert_eq!(vec![(String::from("test"), 0)], extracted); 10 | } 11 | 12 | #[test] 13 | fn test_extract_one_byte() { 14 | let config = BytesConfig::new(b"t\x00".to_vec()).with_min_length(1); 15 | let extracted = strings(&config).unwrap(); 16 | assert_eq!(vec![(String::from("t"), 0)], extracted); 17 | } 18 | 19 | #[test] 20 | fn test_extract_bytes_min_length_1() { 21 | let config = BytesConfig::new(b"test\x00".to_vec()).with_min_length(1); 22 | let extracted = strings(&config).unwrap(); 23 | assert_eq!(vec![(String::from("test"), 0)], extracted); 24 | } 25 | 26 | #[test] 27 | fn test_bytes_config_bytes_array() { 28 | let config = BytesConfig::new(b"test\x00".to_vec()); 29 | let extracted = strings(&config).unwrap(); 30 | assert_eq!(vec![(String::from("test"), 0)], extracted); 31 | } 32 | 33 | #[test] 34 | fn test_bytes_config_offset() { 35 | let config = BytesConfig::new(vec![0, 116, 101, 115, 116]); 36 | let extracted = strings(&config).unwrap(); 37 | assert_eq!(vec![(String::from("test"), 1)], extracted); 38 | } 39 | 40 | #[test] 41 | fn test_bytes_config_min_length() { 42 | let config = BytesConfig::new(vec![116, 101, 115, 116, 0, 0, 116, 101, 115]).with_min_length(4); 43 | let extracted = strings(&config).unwrap(); 44 | assert_eq!(vec![(String::from("test"), 0)], extracted); 45 | } 46 | 47 | #[test] 48 | fn test_bytes_config_multiple_strings() { 49 | let config = BytesConfig::new(vec![116, 101, 115, 116, 0, 0, 116, 101, 115]).with_min_length(3); 50 | let extracted = strings(&config).unwrap(); 51 | assert_eq!( 52 | vec![(String::from("test"), 0), (String::from("tes"), 6)], 53 | extracted 54 | ); 55 | } 56 | 57 | #[test] 58 | fn test_file_config() { 59 | let mut file = NamedTempFile::new().unwrap(); 60 | file.write_all(b"test\x00").unwrap(); 61 | 62 | let path = file.path(); 63 | let config = FileConfig::new(path); 64 | let extracted = strings(&config).unwrap(); 65 | assert_eq!(vec![(String::from("test"), 0)], extracted); 66 | } 67 | 68 | #[test] 69 | fn test_utf16le() { 70 | let config = 71 | BytesConfig::new(b"t\x00e\x00s\x00t\x00\x00\x00".to_vec()).with_encoding(Encoding::UTF16LE); 72 | let extracted = strings(&config).unwrap(); 73 | assert_eq!(vec![(String::from("test"), 0)], extracted); 74 | } 75 | 76 | #[test] 77 | fn test_utf16be() { 78 | let config = BytesConfig::new(b"\x00t\x00e\x00s\x00t\x00\x00\x00".to_vec()) 79 | .with_encoding(Encoding::UTF16BE); 80 | let extracted = strings(&config).unwrap(); 81 | assert_eq!(vec![(String::from("test"), 0)], extracted); 82 | } 83 | 84 | #[test] 85 | fn test_multiple_encodings() { 86 | let config = BytesConfig::new(b"ascii\x01t\x00e\x00s\x00t\x00\x00\x00".to_vec()) 87 | .with_encoding(Encoding::ASCII) 88 | .with_encoding(Encoding::UTF16LE); 89 | let extracted = strings(&config).unwrap(); 90 | assert_eq!( 91 | vec![(String::from("ascii"), 0), (String::from("test"), 6)], 92 | extracted 93 | ); 94 | } 95 | 96 | #[test] 97 | fn test_json_dump() { 98 | let file = NamedTempFile::new().unwrap(); 99 | let config = BytesConfig::new(b"\x00\x00test\"\n\tmore\x00\x00".to_vec()); 100 | 101 | let path = file.path().to_path_buf(); 102 | dump_strings(&config, path).unwrap(); 103 | let mut string = String::new(); 104 | file.as_file().read_to_string(&mut string).unwrap(); 105 | assert_eq!(string, String::from("[[\"test\\\"\\n\\tmore\",2]]")); 106 | } 107 | 108 | #[test] 109 | fn test_json_dump_multiple_strings() { 110 | let file = NamedTempFile::new().unwrap(); 111 | let config = BytesConfig::new(b"\x00\x00test\"\n\tmore\x00\x00more text over here".to_vec()); 112 | 113 | let path = file.path().to_path_buf(); 114 | dump_strings(&config, path).unwrap(); 115 | let mut string = String::new(); 116 | file.as_file().read_to_string(&mut string).unwrap(); 117 | assert_eq!( 118 | string, 119 | String::from("[[\"test\\\"\\n\\tmore\",2],[\"more text over here\",15]]") 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/release_python.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | tags: 5 | - "v**" 6 | 7 | name: Python release 8 | 9 | env: 10 | name: rust-strings 11 | 12 | jobs: 13 | macos: 14 | runs-on: macos-latest 15 | strategy: 16 | matrix: 17 | python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python_version }} 23 | architecture: x64 24 | - name: Install Rust toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | profile: minimal 29 | default: true 30 | - name: Build wheels - x86_64 31 | uses: PyO3/maturin-action@v1 32 | with: 33 | target: x86_64 34 | args: --release --features python_bindings --out dist 35 | - name: Install built wheel - x86_64 36 | run: | 37 | pip install ${{ env.name }} --no-index --find-links dist --force-reinstall 38 | pip install pytest 39 | pytest 40 | - name: Build wheels - universal2 41 | uses: PyO3/maturin-action@v1 42 | with: 43 | args: --release -i python${{ matrix.python_version }} --features python_bindings --universal2 --out dist 44 | - name: Install built wheel - universal2 45 | run: | 46 | pip install ${{ env.name }} --no-index --find-links dist --force-reinstall 47 | pytest 48 | - name: Upload wheels 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: wheels 52 | path: dist 53 | 54 | windows: 55 | runs-on: windows-latest 56 | strategy: 57 | matrix: 58 | target: [x64, x86] 59 | python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: actions/setup-python@v5 63 | with: 64 | python-version: ${{ matrix.python_version }} 65 | architecture: ${{ matrix.target }} 66 | - name: Install Rust toolchain 67 | uses: actions-rs/toolchain@v1 68 | with: 69 | toolchain: stable 70 | profile: minimal 71 | default: true 72 | - name: Build wheels 73 | uses: PyO3/maturin-action@v1 74 | with: 75 | target: ${{ matrix.target }} 76 | args: --release -i python${{ matrix.python_version }} --features python_bindings --out dist 77 | - name: Install built wheel 78 | run: | 79 | pip install ${{ env.name }} --no-index --find-links dist --force-reinstall 80 | pip install pytest 81 | pytest 82 | - name: Upload wheels 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: wheels 86 | path: dist 87 | 88 | linux: 89 | runs-on: ubuntu-latest 90 | strategy: 91 | matrix: 92 | target: [x86_64, i686] 93 | python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 94 | steps: 95 | - uses: actions/checkout@v4 96 | - uses: actions/setup-python@v5 97 | with: 98 | python-version: ${{ matrix.python_version }} 99 | architecture: x64 100 | - name: Build Wheels 101 | uses: PyO3/maturin-action@v1 102 | with: 103 | rust-toolchain: stable 104 | target: ${{ matrix.target }} 105 | manylinux: auto 106 | args: --release -i python${{ matrix.python_version }} --features python_bindings --out dist 107 | - name: Install built wheel 108 | if: matrix.target == 'x86_64' 109 | run: | 110 | pip install ${{ env.name }} --no-index --find-links dist --force-reinstall 111 | pip install pytest 112 | pytest 113 | - name: Upload wheels 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: wheels 117 | path: dist 118 | 119 | linux-cross: 120 | runs-on: ubuntu-latest 121 | strategy: 122 | matrix: 123 | target: [aarch64, armv7, s390x, ppc64le, ppc64] 124 | python: [ 125 | { version: '3.7', abi: 'cp37-cp37m' }, 126 | { version: '3.8', abi: 'cp38-cp38' }, 127 | { version: '3.9', abi: 'cp39-cp39' }, 128 | { version: '3.10', abi: 'cp310-cp310' }, 129 | { version: '3.11', abi: 'cp311-cp311' }, 130 | { version: '3.12', abi: 'cp312-cp312' }, 131 | ] 132 | steps: 133 | - uses: actions/checkout@v4 134 | - name: Build Wheels 135 | uses: PyO3/maturin-action@v1 136 | env: 137 | PYO3_CROSS_LIB_DIR: /opt/python/${{ matrix.python.abi }} 138 | with: 139 | rust-toolchain: stable 140 | target: ${{ matrix.target }} 141 | manylinux: auto 142 | args: --release -i python${{ matrix.python.version }} --features python_bindings --out dist 143 | - name: Upload wheels 144 | uses: actions/upload-artifact@v4 145 | with: 146 | name: wheels 147 | path: dist 148 | 149 | release: 150 | name: Release 151 | runs-on: ubuntu-latest 152 | needs: 153 | - macos 154 | - windows 155 | - linux-cross 156 | - linux 157 | steps: 158 | - uses: actions/download-artifact@v4 159 | with: 160 | name: wheels 161 | path: dist 162 | - name: Publish to PyPi 163 | env: 164 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 165 | uses: PyO3/maturin-action@v1 166 | with: 167 | command: upload 168 | args: --skip-existing dist/* 169 | -------------------------------------------------------------------------------- /src/strings_extractor.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::mem::take; 3 | use std::rc::Rc; 4 | 5 | use crate::encodings::Encoding; 6 | use crate::strings_writer::StringWriter; 7 | use crate::ErrorResult; 8 | 9 | pub trait StringsExtractor { 10 | fn can_consume(&self, c: u8) -> bool; 11 | fn consume(&mut self, offset: u64, c: u8) -> ErrorResult; 12 | fn stop_consume(&mut self) -> ErrorResult; 13 | } 14 | 15 | pub struct AsciiExtractor { 16 | writer: Rc>, 17 | min_length: usize, 18 | current_string: Vec, 19 | offset: u64, 20 | is_start_writing: bool, 21 | } 22 | 23 | pub struct Utf16Extractor { 24 | writer: Rc>, 25 | is_big_endian: bool, 26 | is_last_char_null: Option, 27 | min_length: usize, 28 | current_string: Vec, 29 | offset: Option, 30 | is_start_writing: bool, 31 | } 32 | 33 | pub fn new_strings_extractor<'a, T>( 34 | writer: Rc>, 35 | encoding: Encoding, 36 | min_length: usize, 37 | ) -> Box 38 | where 39 | T: StringWriter + 'a, 40 | { 41 | match encoding { 42 | Encoding::ASCII => Box::new(AsciiExtractor { 43 | writer, 44 | min_length, 45 | current_string: Vec::with_capacity(min_length), 46 | offset: 0, 47 | is_start_writing: false, 48 | }), 49 | Encoding::UTF16LE => Box::new(Utf16Extractor { 50 | writer, 51 | is_big_endian: false, 52 | is_last_char_null: None, 53 | min_length, 54 | current_string: Vec::with_capacity(min_length), 55 | offset: None, 56 | is_start_writing: false, 57 | }), 58 | Encoding::UTF16BE => Box::new(Utf16Extractor { 59 | writer, 60 | is_big_endian: true, 61 | is_last_char_null: None, 62 | min_length, 63 | current_string: Vec::with_capacity(min_length), 64 | offset: None, 65 | is_start_writing: false, 66 | }), 67 | } 68 | } 69 | 70 | fn is_printable_character(c: u8) -> bool { 71 | (32..=126).contains(&c) || (9..=10).contains(&c) || c == 13 72 | } 73 | 74 | impl StringsExtractor for AsciiExtractor 75 | where 76 | T: StringWriter, 77 | { 78 | fn can_consume(&self, c: u8) -> bool { 79 | is_printable_character(c) 80 | } 81 | 82 | fn consume(&mut self, offset: u64, c: u8) -> ErrorResult { 83 | if self.is_start_writing { 84 | self.writer.borrow_mut().write_char(c as char)?; 85 | } else if self.current_string.len() == self.min_length - 1 && !self.is_start_writing { 86 | // Fix case when min_length=1 87 | if self.current_string.is_empty() { 88 | self.offset = offset; 89 | } 90 | self.is_start_writing = true; 91 | self.current_string.push(c); 92 | self.writer 93 | .borrow_mut() 94 | .start_string_consume(take(&mut self.current_string), self.offset)?; 95 | } else if self.current_string.is_empty() && !self.is_start_writing { 96 | self.offset = offset; 97 | self.current_string.push(c); 98 | } else { 99 | self.current_string.push(c); 100 | } 101 | Ok(()) 102 | } 103 | 104 | fn stop_consume(&mut self) -> ErrorResult { 105 | if self.is_start_writing { 106 | self.writer.borrow_mut().finish_string_consume()?; 107 | } 108 | self.is_start_writing = false; 109 | self.current_string.clear(); 110 | Ok(()) 111 | } 112 | } 113 | 114 | impl StringsExtractor for Utf16Extractor 115 | where 116 | T: StringWriter, 117 | { 118 | fn can_consume(&self, c: u8) -> bool { 119 | let is_char_null = c == 0; 120 | match self.is_last_char_null { 121 | None => { 122 | (self.is_big_endian && is_char_null) 123 | || (!self.is_big_endian && is_printable_character(c)) 124 | } 125 | Some(is_last_char_null) => { 126 | let is_char_printable = is_printable_character(c); 127 | (!is_last_char_null && is_char_null) || (is_last_char_null && is_char_printable) 128 | } 129 | } 130 | } 131 | 132 | fn consume(&mut self, offset: u64, c: u8) -> ErrorResult { 133 | let is_char_null = c == 0; 134 | self.is_last_char_null = Some(is_char_null); 135 | if is_char_null { 136 | // This is here because big endian is null first 137 | if self.current_string.is_empty() { 138 | self.offset = Some(offset); 139 | } 140 | return Ok(()); 141 | } 142 | if self.is_start_writing { 143 | self.writer.borrow_mut().write_char(c as char)?; 144 | } else if self.current_string.is_empty() && !self.is_start_writing { 145 | if self.offset.is_none() { 146 | self.offset = Some(offset); 147 | } 148 | self.current_string.push(c); 149 | } else if self.current_string.len() == self.min_length - 1 && !self.is_start_writing { 150 | self.is_start_writing = true; 151 | self.current_string.push(c); 152 | self.writer 153 | .borrow_mut() 154 | .start_string_consume(take(&mut self.current_string), self.offset.unwrap())?; 155 | } else { 156 | self.current_string.push(c); 157 | } 158 | Ok(()) 159 | } 160 | 161 | fn stop_consume(&mut self) -> ErrorResult { 162 | if self.is_start_writing { 163 | self.writer.borrow_mut().finish_string_consume()?; 164 | } 165 | self.is_last_char_null = None; 166 | self.is_start_writing = false; 167 | self.offset = None; 168 | self.current_string.clear(); 169 | Ok(()) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/python_bindings.rs: -------------------------------------------------------------------------------- 1 | use pyo3::create_exception; 2 | use pyo3::exceptions::PyException; 3 | use pyo3::prelude::*; 4 | use std::error::Error; 5 | use std::path::PathBuf; 6 | use std::str::FromStr; 7 | 8 | use crate::encodings::EncodingNotFoundError; 9 | use crate::{ 10 | dump_strings as r_dump_strings, strings as r_strings, BytesConfig as RustBytesConfig, 11 | Encoding as RustEncoding, ErrorResult, FileConfig as RustFileConfig, 12 | }; 13 | 14 | create_exception!(pystrings, StringsException, PyException); 15 | create_exception!(pystrings, EncodingNotFoundException, StringsException); 16 | 17 | impl From for PyErr { 18 | fn from(err: EncodingNotFoundError) -> PyErr { 19 | EncodingNotFoundException::new_err(format!("{}", err)) 20 | } 21 | } 22 | 23 | /// Extract strings from binary file or bytes. 24 | /// :param file_path: path to file (can't be with bytes option) 25 | /// :param bytes: bytes (can't be with file_path option) 26 | /// :param min_length: strings minimum length 27 | /// :param encoding: strings encoding (default is ["ascii"]) 28 | /// :param buffer_size: the buffer size to read the file (relevant only to file_path option) 29 | /// :return: list of tuples of string and offset 30 | /// :raises: raise StringsException if there is any error during string extraction 31 | /// raise EncodingNotFoundException if the function got an unsupported encondings 32 | #[pyfunction()] 33 | #[pyo3(signature=( 34 | file_path = None, 35 | bytes = None, 36 | min_length = 3, 37 | encodings = vec!["ascii"], 38 | buffer_size = 1024 * 1024 39 | ))] 40 | #[pyo3( 41 | text_signature = "(file_path: Optional[Union[str, Path]] = None, bytes: Optional[bytes] = None, min_length: int = 3, encoding: List[str] = [\"ascii\"], buffer_size: int = 1024 * 1024) -> List[Tuple[str, int]]" 42 | )] 43 | fn strings( 44 | py: Python<'_>, 45 | file_path: Option, 46 | bytes: Option>, 47 | min_length: usize, 48 | encodings: Vec<&str>, 49 | buffer_size: usize, 50 | ) -> PyResult> { 51 | py.allow_threads(|| { 52 | if matches!(file_path, Some(_)) && matches!(bytes, Some(_)) { 53 | return Err(StringsException::new_err( 54 | "You can't specify file_path and bytes", 55 | )); 56 | } 57 | let encodings = encodings 58 | .iter() 59 | .map(|e| RustEncoding::from_str(e)) 60 | .collect::, _>>()?; 61 | let result: Result, Box>; 62 | if let Some(file_path) = file_path { 63 | let strings_config = RustFileConfig::new(&file_path) 64 | .with_min_length(min_length) 65 | .with_encodings(encodings) 66 | .with_buffer_size(buffer_size); 67 | result = r_strings(&strings_config); 68 | } else if let Some(bytes) = bytes { 69 | let strings_config = RustBytesConfig::new(bytes) 70 | .with_min_length(min_length) 71 | .with_encodings(encodings); 72 | result = r_strings(&strings_config); 73 | } else { 74 | return Err(StringsException::new_err( 75 | "You must specify file_path or bytes", 76 | )); 77 | } 78 | if let Err(error_message) = result { 79 | return Err(StringsException::new_err(format!("{}", error_message))); 80 | } 81 | Ok(result.unwrap()) 82 | }) 83 | } 84 | 85 | /// Dump strings from binary file or bytes to json file. 86 | /// :param output_file: path to file to dump into 87 | /// :param file_path: path to file (can't be with bytes option) 88 | /// :param bytes: bytes (can't be with file_path option) 89 | /// :param min_length: strings minimum length 90 | /// :param encoding: strings encoding (default is ["ascii"]) 91 | /// :param buffer_size: the buffer size to read the file (relevant only to file_path option) 92 | /// :return: list of tuples of string and offset 93 | /// :raises: raise StringsException if there is any error during string extraction 94 | /// raise EncodingNotFoundException if the function got an unsupported encondings 95 | #[pyfunction()] 96 | #[pyo3(signature=( 97 | output_file, 98 | file_path = None, 99 | bytes = None, 100 | min_length = 3, 101 | encodings = vec!["ascii"], 102 | buffer_size = 1024 * 1024 103 | ))] 104 | #[pyo3( 105 | text_signature = "(output_file: str, file_path: Optional[Union[str, Path]] = None, bytes: Optional[bytes] = None, min_length: int = 3, encoding: List[str] = [\"ascii\"], buffer_size: int = 1024 * 1024) -> None" 106 | )] 107 | fn dump_strings( 108 | py: Python<'_>, 109 | output_file: PathBuf, 110 | file_path: Option, 111 | bytes: Option>, 112 | min_length: usize, 113 | encodings: Vec<&str>, 114 | buffer_size: usize, 115 | ) -> PyResult<()> { 116 | py.allow_threads(|| { 117 | if matches!(file_path, Some(_)) && matches!(bytes, Some(_)) { 118 | return Err(StringsException::new_err( 119 | "You can't specify file_path and bytes", 120 | )); 121 | } 122 | let encodings = encodings 123 | .iter() 124 | .map(|e| RustEncoding::from_str(e)) 125 | .collect::, _>>()?; 126 | let result: ErrorResult; 127 | if let Some(file_path) = file_path { 128 | let strings_config = RustFileConfig::new(&file_path) 129 | .with_min_length(min_length) 130 | .with_encodings(encodings) 131 | .with_buffer_size(buffer_size); 132 | result = r_dump_strings(&strings_config, output_file); 133 | } else if let Some(bytes) = bytes { 134 | let strings_config = RustBytesConfig::new(bytes) 135 | .with_min_length(min_length) 136 | .with_encodings(encodings); 137 | result = r_dump_strings(&strings_config, output_file); 138 | } else { 139 | return Err(StringsException::new_err( 140 | "You must specify file_path or bytes", 141 | )); 142 | } 143 | if let Err(error_message) = result { 144 | return Err(StringsException::new_err(format!("{}", error_message))); 145 | } 146 | Ok(()) 147 | }) 148 | } 149 | 150 | #[pymodule] 151 | #[pyo3(name = "rust_strings")] 152 | fn rust_strings(m: &Bound<'_, PyModule>) -> PyResult<()> { 153 | m.add_function(wrap_pyfunction!(strings, m)?)?; 154 | m.add_function(wrap_pyfunction!(dump_strings, m)?)?; 155 | m.add( 156 | "StringsException", 157 | m.py().get_type_bound::(), 158 | )?; 159 | m.add( 160 | "EncodingNotFoundException", 161 | m.py().get_type_bound::(), 162 | )?; 163 | Ok(()) 164 | } 165 | -------------------------------------------------------------------------------- /src/strings.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::error::Error; 3 | use std::fs::File; 4 | use std::io::{BufReader, Read}; 5 | use std::iter::Iterator; 6 | use std::path::{Path, PathBuf}; 7 | use std::rc::Rc; 8 | use std::result::Result; 9 | 10 | use crate::encodings::Encoding; 11 | use crate::strings_extractor::{new_strings_extractor, StringsExtractor}; 12 | use crate::strings_writer::{JsonWriter, StringWriter, VectorWriter}; 13 | use crate::ErrorResult; 14 | 15 | const DEFAULT_MIN_LENGTH: usize = 3; 16 | const DEFAULT_ENCODINGS: [Encoding; 1] = [Encoding::ASCII]; 17 | 18 | pub trait Config { 19 | #[doc(hidden)] 20 | fn consume(&self, func: F) -> ErrorResult 21 | where 22 | F: FnMut(usize, u8) -> ErrorResult; 23 | #[doc(hidden)] 24 | fn get_min_length(&self) -> usize; 25 | #[doc(hidden)] 26 | fn get_encodings(&self) -> Vec; 27 | } 28 | 29 | macro_rules! impl_config { 30 | () => { 31 | fn get_min_length(&self) -> usize { 32 | self.min_length 33 | } 34 | fn get_encodings(&self) -> Vec { 35 | if self.encodings.is_empty() { 36 | return DEFAULT_ENCODINGS.to_vec(); 37 | } 38 | self.encodings.clone() 39 | } 40 | }; 41 | } 42 | 43 | macro_rules! impl_default { 44 | () => { 45 | pub fn with_min_length(mut self, min_length: usize) -> Self { 46 | self.min_length = min_length; 47 | self 48 | } 49 | 50 | pub fn with_encoding(mut self, encoding: Encoding) -> Self { 51 | self.encodings.push(encoding); 52 | self 53 | } 54 | 55 | pub fn with_encodings(mut self, encodings: Vec) -> Self { 56 | self.encodings = encodings; 57 | self 58 | } 59 | }; 60 | } 61 | 62 | pub struct FileConfig<'a> { 63 | pub file_path: &'a Path, 64 | pub min_length: usize, 65 | pub encodings: Vec, 66 | pub buffer_size: usize, 67 | } 68 | 69 | impl<'a> FileConfig<'a> { 70 | const DEFAULT_BUFFER_SIZE: usize = 1024 * 1024; 71 | 72 | pub fn new(file_path: &'a Path) -> Self { 73 | FileConfig { 74 | file_path, 75 | min_length: DEFAULT_MIN_LENGTH, 76 | encodings: vec![], 77 | buffer_size: FileConfig::DEFAULT_BUFFER_SIZE, 78 | } 79 | } 80 | 81 | pub fn with_buffer_size(mut self, buffer_size: usize) -> Self { 82 | self.buffer_size = buffer_size; 83 | self 84 | } 85 | 86 | impl_default!(); 87 | } 88 | 89 | impl<'a> Config for FileConfig<'a> { 90 | fn consume(&self, mut func: F) -> ErrorResult 91 | where 92 | F: FnMut(usize, u8) -> ErrorResult, 93 | { 94 | let file_result = File::open(self.file_path); 95 | if let Err(err) = file_result { 96 | return Err(Box::new(err)); 97 | } 98 | let file = file_result.unwrap(); 99 | let buf_reader = BufReader::with_capacity(self.buffer_size, file); 100 | buf_reader 101 | .bytes() 102 | .enumerate() 103 | .try_for_each(|(i, b)| func(i, b.unwrap()))?; 104 | Ok(()) 105 | } 106 | 107 | impl_config!(); 108 | } 109 | 110 | pub struct StdinConfig { 111 | pub min_length: usize, 112 | pub encodings: Vec, 113 | pub buffer_size: usize, 114 | } 115 | 116 | impl Default for StdinConfig { 117 | fn default() -> Self { 118 | Self::new() 119 | } 120 | } 121 | 122 | impl StdinConfig { 123 | const DEFAULT_BUFFER_SIZE: usize = 1024 * 1024; 124 | 125 | pub fn new() -> Self { 126 | StdinConfig { 127 | min_length: DEFAULT_MIN_LENGTH, 128 | encodings: vec![], 129 | buffer_size: StdinConfig::DEFAULT_BUFFER_SIZE, 130 | } 131 | } 132 | 133 | pub fn with_buffer_size(mut self, buffer_size: usize) -> Self { 134 | self.buffer_size = buffer_size; 135 | self 136 | } 137 | 138 | impl_default!(); 139 | } 140 | 141 | impl Config for StdinConfig { 142 | fn consume(&self, mut func: F) -> ErrorResult 143 | where 144 | F: FnMut(usize, u8) -> ErrorResult, 145 | { 146 | let buf_reader = BufReader::with_capacity(self.buffer_size, std::io::stdin()); 147 | buf_reader 148 | .bytes() 149 | .enumerate() 150 | .try_for_each(|(i, b)| func(i, b.unwrap()))?; 151 | Ok(()) 152 | } 153 | 154 | impl_config!(); 155 | } 156 | 157 | pub struct BytesConfig { 158 | pub bytes: Vec, 159 | pub min_length: usize, 160 | pub encodings: Vec, 161 | } 162 | 163 | impl BytesConfig { 164 | pub fn new(bytes: Vec) -> Self { 165 | BytesConfig { 166 | bytes, 167 | min_length: DEFAULT_MIN_LENGTH, 168 | encodings: vec![], 169 | } 170 | } 171 | 172 | impl_default!(); 173 | } 174 | 175 | impl Config for BytesConfig { 176 | fn consume(&self, mut func: F) -> ErrorResult 177 | where 178 | F: FnMut(usize, u8) -> ErrorResult, 179 | { 180 | self.bytes 181 | .iter() 182 | .enumerate() 183 | .try_for_each(|(i, b)| func(i, *b))?; 184 | Ok(()) 185 | } 186 | 187 | impl_config!(); 188 | } 189 | 190 | fn _strings( 191 | strings_config: &T, 192 | strings_writer: Rc>, 193 | ) -> ErrorResult { 194 | let min_length = strings_config.get_min_length(); 195 | let mut strings_extractors: Vec> = strings_config 196 | .get_encodings() 197 | .iter() 198 | .map(|e| new_strings_extractor(strings_writer.clone(), *e, min_length)) 199 | .collect(); 200 | strings_config.consume(|offset: usize, c: u8| { 201 | strings_extractors 202 | .iter_mut() 203 | .try_for_each(|strings_extractor| -> ErrorResult { 204 | if strings_extractor.can_consume(c) { 205 | strings_extractor.consume(offset as u64, c)?; 206 | } else { 207 | strings_extractor.stop_consume()?; 208 | } 209 | Ok(()) 210 | })?; 211 | Ok(()) 212 | })?; 213 | strings_extractors 214 | .iter_mut() 215 | .try_for_each(|strings_extractor| -> ErrorResult { 216 | strings_extractor.stop_consume()?; 217 | Ok(()) 218 | })?; 219 | Ok(()) 220 | } 221 | 222 | /// Extract strings from binary data. 223 | /// 224 | /// Examples: 225 | /// ``` 226 | /// use rust_strings::{FileConfig, BytesConfig, strings, Encoding}; 227 | /// use std::path::Path; 228 | /// 229 | /// let config = FileConfig::new(Path::new("/bin/ls")).with_min_length(5); 230 | /// let extracted_strings = strings(&config); 231 | /// 232 | /// // Extract utf16le strings 233 | /// let config = FileConfig::new(Path::new("C:\\Windows\\notepad.exe")) 234 | /// .with_min_length(15) 235 | /// .with_encoding(Encoding::UTF16LE); 236 | /// let extracted_strings = strings(&config); 237 | /// 238 | /// // Extract ascii and utf16le strings 239 | /// let config = FileConfig::new(Path::new("C:\\Windows\\notepad.exe")) 240 | /// .with_min_length(15) 241 | /// .with_encoding(Encoding::ASCII) 242 | /// .with_encoding(Encoding::UTF16LE); 243 | /// let extracted_strings = strings(&config); 244 | /// 245 | /// let config = BytesConfig::new(b"test\x00".to_vec()); 246 | /// let extracted_strings = strings(&config); 247 | /// assert_eq!(vec![(String::from("test"), 0)], extracted_strings.unwrap()); 248 | /// ``` 249 | pub fn strings(strings_config: &T) -> Result, Box> { 250 | let vector_writer = Rc::new(RefCell::new(VectorWriter::new())); 251 | _strings(strings_config, vector_writer.clone())?; 252 | let result = Ok(vector_writer.borrow_mut().get_strings()); 253 | result 254 | } 255 | 256 | /// Dump strings from binary data to json file. 257 | /// 258 | /// Examples: 259 | /// ``` 260 | /// use std::path::PathBuf; 261 | /// use rust_strings::{BytesConfig, dump_strings}; 262 | /// 263 | /// let config = BytesConfig::new(b"test\x00".to_vec()); 264 | /// dump_strings(&config, PathBuf::from("strings.json")); 265 | /// 266 | pub fn dump_strings(strings_config: &T, output: PathBuf) -> ErrorResult { 267 | let output_file = File::create(output)?; 268 | let vector_writer = Rc::new(RefCell::new(JsonWriter::new(output_file))); 269 | _strings(strings_config, vector_writer.clone())?; 270 | vector_writer.borrow_mut().finish()?; 271 | Ok(()) 272 | } 273 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.14" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.7" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.4" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.0.3" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "autocfg" 56 | version = "1.3.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 59 | 60 | [[package]] 61 | name = "bitflags" 62 | version = "2.5.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 65 | 66 | [[package]] 67 | name = "cfg-if" 68 | version = "1.0.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 71 | 72 | [[package]] 73 | name = "clap" 74 | version = "4.5.8" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" 77 | dependencies = [ 78 | "clap_builder", 79 | "clap_derive", 80 | ] 81 | 82 | [[package]] 83 | name = "clap_builder" 84 | version = "4.5.8" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" 87 | dependencies = [ 88 | "anstream", 89 | "anstyle", 90 | "clap_lex", 91 | "strsim", 92 | ] 93 | 94 | [[package]] 95 | name = "clap_derive" 96 | version = "4.5.8" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" 99 | dependencies = [ 100 | "heck 0.5.0", 101 | "proc-macro2", 102 | "quote", 103 | "syn", 104 | ] 105 | 106 | [[package]] 107 | name = "clap_lex" 108 | version = "0.7.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 111 | 112 | [[package]] 113 | name = "colorchoice" 114 | version = "1.0.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 117 | 118 | [[package]] 119 | name = "errno" 120 | version = "0.3.8" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 123 | dependencies = [ 124 | "libc", 125 | "windows-sys", 126 | ] 127 | 128 | [[package]] 129 | name = "fastrand" 130 | version = "2.1.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 133 | 134 | [[package]] 135 | name = "heck" 136 | version = "0.4.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 139 | 140 | [[package]] 141 | name = "heck" 142 | version = "0.5.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 145 | 146 | [[package]] 147 | name = "indoc" 148 | version = "2.0.5" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 151 | 152 | [[package]] 153 | name = "is_terminal_polyfill" 154 | version = "1.70.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 157 | 158 | [[package]] 159 | name = "libc" 160 | version = "0.2.154" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 163 | 164 | [[package]] 165 | name = "linux-raw-sys" 166 | version = "0.4.13" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 169 | 170 | [[package]] 171 | name = "lock_api" 172 | version = "0.4.12" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 175 | dependencies = [ 176 | "autocfg", 177 | "scopeguard", 178 | ] 179 | 180 | [[package]] 181 | name = "memoffset" 182 | version = "0.9.1" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 185 | dependencies = [ 186 | "autocfg", 187 | ] 188 | 189 | [[package]] 190 | name = "once_cell" 191 | version = "1.19.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 194 | 195 | [[package]] 196 | name = "parking_lot" 197 | version = "0.12.2" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" 200 | dependencies = [ 201 | "lock_api", 202 | "parking_lot_core", 203 | ] 204 | 205 | [[package]] 206 | name = "parking_lot_core" 207 | version = "0.9.10" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 210 | dependencies = [ 211 | "cfg-if", 212 | "libc", 213 | "redox_syscall", 214 | "smallvec", 215 | "windows-targets", 216 | ] 217 | 218 | [[package]] 219 | name = "portable-atomic" 220 | version = "1.6.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 223 | 224 | [[package]] 225 | name = "proc-macro2" 226 | version = "1.0.81" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 229 | dependencies = [ 230 | "unicode-ident", 231 | ] 232 | 233 | [[package]] 234 | name = "pyo3" 235 | version = "0.21.2" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" 238 | dependencies = [ 239 | "cfg-if", 240 | "indoc", 241 | "libc", 242 | "memoffset", 243 | "parking_lot", 244 | "portable-atomic", 245 | "pyo3-build-config", 246 | "pyo3-ffi", 247 | "pyo3-macros", 248 | "unindent", 249 | ] 250 | 251 | [[package]] 252 | name = "pyo3-build-config" 253 | version = "0.21.2" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" 256 | dependencies = [ 257 | "once_cell", 258 | "target-lexicon", 259 | ] 260 | 261 | [[package]] 262 | name = "pyo3-ffi" 263 | version = "0.21.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" 266 | dependencies = [ 267 | "libc", 268 | "pyo3-build-config", 269 | ] 270 | 271 | [[package]] 272 | name = "pyo3-macros" 273 | version = "0.21.2" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" 276 | dependencies = [ 277 | "proc-macro2", 278 | "pyo3-macros-backend", 279 | "quote", 280 | "syn", 281 | ] 282 | 283 | [[package]] 284 | name = "pyo3-macros-backend" 285 | version = "0.21.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" 288 | dependencies = [ 289 | "heck 0.4.1", 290 | "proc-macro2", 291 | "pyo3-build-config", 292 | "quote", 293 | "syn", 294 | ] 295 | 296 | [[package]] 297 | name = "quote" 298 | version = "1.0.36" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 301 | dependencies = [ 302 | "proc-macro2", 303 | ] 304 | 305 | [[package]] 306 | name = "redox_syscall" 307 | version = "0.5.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 310 | dependencies = [ 311 | "bitflags", 312 | ] 313 | 314 | [[package]] 315 | name = "rust-strings" 316 | version = "0.6.0" 317 | dependencies = [ 318 | "clap", 319 | "pyo3", 320 | "tempfile", 321 | ] 322 | 323 | [[package]] 324 | name = "rustix" 325 | version = "0.38.34" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 328 | dependencies = [ 329 | "bitflags", 330 | "errno", 331 | "libc", 332 | "linux-raw-sys", 333 | "windows-sys", 334 | ] 335 | 336 | [[package]] 337 | name = "scopeguard" 338 | version = "1.2.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 341 | 342 | [[package]] 343 | name = "smallvec" 344 | version = "1.13.2" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 347 | 348 | [[package]] 349 | name = "strsim" 350 | version = "0.11.1" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 353 | 354 | [[package]] 355 | name = "syn" 356 | version = "2.0.60" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 359 | dependencies = [ 360 | "proc-macro2", 361 | "quote", 362 | "unicode-ident", 363 | ] 364 | 365 | [[package]] 366 | name = "target-lexicon" 367 | version = "0.12.14" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" 370 | 371 | [[package]] 372 | name = "tempfile" 373 | version = "3.10.1" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 376 | dependencies = [ 377 | "cfg-if", 378 | "fastrand", 379 | "rustix", 380 | "windows-sys", 381 | ] 382 | 383 | [[package]] 384 | name = "unicode-ident" 385 | version = "1.0.12" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 388 | 389 | [[package]] 390 | name = "unindent" 391 | version = "0.2.3" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 394 | 395 | [[package]] 396 | name = "utf8parse" 397 | version = "0.2.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 400 | 401 | [[package]] 402 | name = "windows-sys" 403 | version = "0.52.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 406 | dependencies = [ 407 | "windows-targets", 408 | ] 409 | 410 | [[package]] 411 | name = "windows-targets" 412 | version = "0.52.5" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 415 | dependencies = [ 416 | "windows_aarch64_gnullvm", 417 | "windows_aarch64_msvc", 418 | "windows_i686_gnu", 419 | "windows_i686_gnullvm", 420 | "windows_i686_msvc", 421 | "windows_x86_64_gnu", 422 | "windows_x86_64_gnullvm", 423 | "windows_x86_64_msvc", 424 | ] 425 | 426 | [[package]] 427 | name = "windows_aarch64_gnullvm" 428 | version = "0.52.5" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 431 | 432 | [[package]] 433 | name = "windows_aarch64_msvc" 434 | version = "0.52.5" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 437 | 438 | [[package]] 439 | name = "windows_i686_gnu" 440 | version = "0.52.5" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 443 | 444 | [[package]] 445 | name = "windows_i686_gnullvm" 446 | version = "0.52.5" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 449 | 450 | [[package]] 451 | name = "windows_i686_msvc" 452 | version = "0.52.5" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 455 | 456 | [[package]] 457 | name = "windows_x86_64_gnu" 458 | version = "0.52.5" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 461 | 462 | [[package]] 463 | name = "windows_x86_64_gnullvm" 464 | version = "0.52.5" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 467 | 468 | [[package]] 469 | name = "windows_x86_64_msvc" 470 | version = "0.52.5" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 473 | --------------------------------------------------------------------------------