Repository navigation
feat: Stringy v1.0 -- pipeline architecture, CLI hardening, and comprehensive testing - #141
Conversation
- Deleted the `concept.md` file which outlined the Stringy tool's principles, architecture, and features. - Removed the `2026-02-16-extraction-refactor-and-cli.md` plan detailing the refactor of the extraction module and CLI integration tasks. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…stency Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…ction functions Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
… documentation Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…rors - Add assert_cmd + predicates for declarative CLI integration tests - Add patharg for idiomatic file/stdin input (supports "-" for stdin) - Rewrite and expand CLI tests from 4 to 18 covering help, version, validation, format conflicts, tag filters, and multi-format binaries - Enrich UnsupportedFormat error to list supported formats (ELF, PE, Mach-O) - Include file path context in I/O error messages - Add author, long_about, and usage examples to --help output Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Add exit code verification tests (code 2 for clap errors, code 1 for runtime errors). Add SerializationError variant to StringyError and migrate JSON serialization errors from ConfigError to the new variant for more precise error categorization. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…action and filtering - Added `pipeline` module with configuration, filtering, and normalization functionalities. - Implemented `Pipeline` struct to orchestrate the entire string extraction process. - Created `FilterEngine` to apply post-extraction filters based on length, encoding, and tags. - Added `ScoreNormalizer` to normalize internal scores for user-friendly display. - Enhanced CLI to support new filtering options and output formats. - Updated output metadata to include binary format and summary options. - Refactored extraction and output logic to integrate with the new pipeline structure. - Added tests for new features and ensured existing functionality remains intact. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
… for common pitfalls Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Replace std::fs::read() with mmap_guard::map_file() for zero-copy read-only access with advisory locking. Empty files are short-circuited to an empty buffer to preserve graceful pipeline behavior. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…troubleshooting - Updated quick start guide to reflect current and planned features, improved output examples, and clarified usage instructions. - Revised ranking algorithm documentation to simplify scoring formula, enhance section weight descriptions, and clarify semantic boosts and noise penalties. - Streamlined troubleshooting section by removing outdated solutions, clarifying error messages, and improving performance issue guidance. - Enhanced consistency in command examples and output formatting across all documentation sections. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Replace bare '|' assertion with ' | ' table separator check to avoid false positives from legitimate strings containing pipe characters. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Replace all Docker-based build instructions with Zig cross-compilation commands. Update AGENTS.md and GOTCHAS.md to reflect the new toolchain. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Add -v flag to zig cc and /v flag to zig rc so gen-fixtures shows linker invocations instead of running silently. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Compiled fixtures (ELF, PE, Mach-O) were already in .gitignore but still tracked. Remove them from the index so gitignore takes effect. Add test_empty.bin and test_unknown.bin as committed static fixtures. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…alysis duration Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…warnings Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…ntent Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…p examples Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…ess and content details Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…mbol extraction test Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
|
|
||
| Basic functionality is implemented with the full interface in development: | ||
| # Exclude noisy tags | ||
| stringy --no-tags format_string target_binary |
There was a problem hiding this comment.
The README example uses --no-tags format_string, but Tag::from_str only accepts fmt for format strings (and serde renames the variant to "fmt"). Update the example to a supported tag value so users can copy/paste it successfully.
| stringy --no-tags format_string target_binary | |
| stringy --no-tags fmt target_binary |
| @@ -142,10 +143,12 @@ Score Offset Section Tags String | |||
| "encoding": "utf-8", | |||
There was a problem hiding this comment.
The JSON output example shows "encoding": "utf-8", but Encoding derives Serialize without serde(rename), so it serializes as "Utf8" (and similarly "Ascii", "Utf16Le", etc.). Please update the README example to match the actual JSON schema.
| "encoding": "utf-8", | |
| "encoding": "Utf8", |
| // NOTE: --summary non-TTY exit 1 is covered in integration_flows_6_7.rs | ||
| // (flow7_summary_non_tty_exit_1). |
There was a problem hiding this comment.
This comment says the non-TTY --summary path exits 1 and references flow7_summary_non_tty_exit_1, but the actual test is flow7_summary_non_tty_exit_2 (validation error -> exit code 2). Update the comment to reflect the current contract to avoid confusion when maintaining the flow matrix.
| /// Sets the display score (debug mode) | ||
| #[must_use] | ||
| pub fn with_display_score(mut self, score: i32) -> Self { | ||
| self.display_score = Some(score); |
There was a problem hiding this comment.
with_display_score is documented as "(debug mode)", but display_score is also used in normal (non-raw) output after score normalization (e.g., TTY table prefers display_score). Please update the doc comment to avoid implying it is debug-only.
| # Output formats | ||
| stringy --json target_binary | ||
| stringy --yara target_binary | ||
| stringy --json target_binary | jq '.[] | select(.tags[] | contains("Url"))' |
There was a problem hiding this comment.
The README pipes --json output into jq '.[] | ...', but stringy --json emits JSONL (one JSON object per line), not a JSON array. Update the example to a JSONL-compatible jq invocation (e.g., processing input line-by-line) so it works as written.
| stringy --json target_binary | jq '.[] | select(.tags[] | contains("Url"))' | |
| stringy --json target_binary | jq 'select(.tags[]? | contains("Url"))' |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
tests/integration_pe.rs (5)
554-556:⚠️ Potential issue | 🟡 MinorAnother tautology:
is_empty() || !is_empty()is always true.The comment indicates the intent is to verify no panic occurs. If that's the goal, remove the assertion entirely - the test passing without panic is sufficient.
Proposed fix
let strings = stringy::extraction::extract_resource_strings(&pe_data); - // Should return empty Vec without panicking - assert!(strings.is_empty() || !strings.is_empty()); // Either is fine, just no panic + // Should return without panicking - either empty or populated Vec is acceptable + let _ = strings; // Silence unused warning; test passes if no panic🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration_pe.rs` around lines 554 - 556, The assertion assert!(strings.is_empty() || !strings.is_empty()) is a tautology and should be removed; simply call stringy::extraction::extract_resource_strings(&pe_data) and either drop or bind the result (e.g., let _ = strings;) so the test verifies "no panic" by completing without assertions—update the test around the variable strings (from extract_resource_strings) accordingly.
121-124:⚠️ Potential issue | 🟡 MinorTautological assertion provides no value.
is_some() || is_none()is always true for anOption. This assertion cannot fail and doesn't verify anything meaningful. Either remove it or assert something specific about the expected state.Proposed fix
- // Verify resources field exists (may be None for simple binaries) - assert!( - container_info.resources.is_some() || container_info.resources.is_none(), - "Resources field should exist in ContainerInfo" - ); + // Resources may be None for simple binaries - no assertion needed, + // the type system guarantees the field exists.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration_pe.rs` around lines 121 - 124, The assertion uses a tautology (container_info.resources.is_some() || container_info.resources.is_none()) which can never fail; replace or remove it. Update the test in integration_pe.rs to assert a concrete expectation about ContainerInfo::resources (for example, assert!(container_info.resources.is_some(), "expected resources to be present") or assert_eq!(container_info.resources, None) as appropriate) or remove the assertion entirely if no specific state is required; reference the container_info variable and its resources field when making the meaningful assertion.
138-146: 🧹 Nitpick | 🔵 TrivialInconsistent fixture-missing behavior across tests.
Tests like
test_pe_resource_enumerationandtest_pe_resource_strings_empty_binarysilently return early when fixtures are missing, while others (e.g.,test_pe_resource_extraction_with_resources) assert fixture presence with detailed build instructions.Silent returns mask CI misconfigurations. Prefer consistent behavior: either all tests assert fixture presence, or use
#[ignore]with a reason for optional fixtures.Also applies to: 546-551
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration_pe.rs` around lines 138 - 146, The test currently swallows a missing PE fixture by printing and returning early (the match on fs::read(&fixture_path) in tests/integration_pe.rs), causing inconsistent behavior across tests; update this test (and the other early-return cases such as in test_pe_resource_enumeration and test_pe_resource_strings_empty_binary) to assert fixture presence instead of silently returning: replace the Err branch with an assert!(false, "...") or panic! containing the same detailed message and build instructions used by test_pe_resource_extraction_with_resources, or alternatively mark the test with #[ignore = "..."] if the fixture is optional—apply the same change consistently to the other affected blocks (lines around the second occurrence) so all tests behave uniformly.
187-190:⚠️ Potential issue | 🟡 MinorSame tautological assertion pattern here.
Identical issue as line 121-124.
Option<T>is always eitherSomeorNoneby definition.Proposed fix
- // Verify the structure is correct even if empty - assert!( - container_info.resources.is_some() || container_info.resources.is_none(), - "Resources field should exist in ContainerInfo" - );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration_pe.rs` around lines 187 - 190, The current assertion checks a tautology on the Option type (container_info.resources.is_some() || container_info.resources.is_none()), which always succeeds; replace it with a meaningful check: either assert that resources is present when the test expects it (use container_info.resources.is_some()) or remove the assertion if absence is acceptable — reference container_info.resources and the ContainerInfo test expectation to make the correct choice and update the assert message accordingly.
1-557: 🧹 Nitpick | 🔵 TrivialFile exceeds 500-line limit (557 lines).
Per coding guidelines, files should stay under 500 lines. Consider splitting into focused modules:
integration_pe_parsing.rs(detection, section classification, imports/exports)integration_pe_resources.rs(resource enumeration, VERSIONINFO, STRINGTABLE extraction)This also improves test isolation and parallel execution.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration_pe.rs` around lines 1 - 557, The file is over the 500-line limit—split tests into two focused integration files: move detection/section/imports/exports/symbol snapshot tests (functions test_pe_import_export_extraction, test_pe_section_classification, test_pe_symbol_extraction_snapshot) into a new tests/integration_pe_parsing.rs and move resource-related tests (test_pe_resource_enumeration, test_pe_resource_extraction_with_resources, test_pe_version_info_string_extraction, test_pe_string_table_extraction, test_pe_resource_string_extraction_snapshot, test_pe_resource_strings_empty_binary) into tests/integration_pe_resources.rs; duplicate or extract the shared helper get_fixture_path and necessary use/import lines into both files (or create a small tests/common util and duplicate if simpler), delete the oversized tests/integration_pe.rs, and run cargo test to ensure names (PeParser, stringy::extraction::extract_resource_strings, and the test function names) still compile and tests run in parallel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@tests/integration_pe.rs`:
- Around line 554-556: The assertion assert!(strings.is_empty() ||
!strings.is_empty()) is a tautology and should be removed; simply call
stringy::extraction::extract_resource_strings(&pe_data) and either drop or bind
the result (e.g., let _ = strings;) so the test verifies "no panic" by
completing without assertions—update the test around the variable strings (from
extract_resource_strings) accordingly.
- Around line 121-124: The assertion uses a tautology
(container_info.resources.is_some() || container_info.resources.is_none()) which
can never fail; replace or remove it. Update the test in integration_pe.rs to
assert a concrete expectation about ContainerInfo::resources (for example,
assert!(container_info.resources.is_some(), "expected resources to be present")
or assert_eq!(container_info.resources, None) as appropriate) or remove the
assertion entirely if no specific state is required; reference the
container_info variable and its resources field when making the meaningful
assertion.
- Around line 138-146: The test currently swallows a missing PE fixture by
printing and returning early (the match on fs::read(&fixture_path) in
tests/integration_pe.rs), causing inconsistent behavior across tests; update
this test (and the other early-return cases such as in
test_pe_resource_enumeration and test_pe_resource_strings_empty_binary) to
assert fixture presence instead of silently returning: replace the Err branch
with an assert!(false, "...") or panic! containing the same detailed message and
build instructions used by test_pe_resource_extraction_with_resources, or
alternatively mark the test with #[ignore = "..."] if the fixture is
optional—apply the same change consistently to the other affected blocks (lines
around the second occurrence) so all tests behave uniformly.
- Around line 187-190: The current assertion checks a tautology on the Option
type (container_info.resources.is_some() || container_info.resources.is_none()),
which always succeeds; replace it with a meaningful check: either assert that
resources is present when the test expects it (use
container_info.resources.is_some()) or remove the assertion if absence is
acceptable — reference container_info.resources and the ContainerInfo test
expectation to make the correct choice and update the assert message
accordingly.
- Around line 1-557: The file is over the 500-line limit—split tests into two
focused integration files: move detection/section/imports/exports/symbol
snapshot tests (functions test_pe_import_export_extraction,
test_pe_section_classification, test_pe_symbol_extraction_snapshot) into a new
tests/integration_pe_parsing.rs and move resource-related tests
(test_pe_resource_enumeration, test_pe_resource_extraction_with_resources,
test_pe_version_info_string_extraction, test_pe_string_table_extraction,
test_pe_resource_string_extraction_snapshot,
test_pe_resource_strings_empty_binary) into tests/integration_pe_resources.rs;
duplicate or extract the shared helper get_fixture_path and necessary use/import
lines into both files (or create a small tests/common util and duplicate if
simpler), delete the oversized tests/integration_pe.rs, and run cargo test to
ensure names (PeParser, stringy::extraction::extract_resource_strings, and the
test function names) still compile and tests run in parallel.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f9a14bd1-cd3a-4fc7-9b4c-210e194f34a0
⛔ Files ignored due to path filters (1)
tests/snapshots/integration_pe__pe_symbol_extraction.snapis excluded by!**/*.snap,!**/tests/snapshots/**
📒 Files selected for processing (2)
Cargo.tomltests/integration_pe.rs
…talled Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
@Mergifyio queue |
Merge Queue Status
This pull request spent 12 minutes in the queue, including 11 minutes 47 seconds running CI. Required conditions to merge
|
Summary
This PR delivers the complete v1.0 implementation of Stringy, transforming it from a prototype into a production-ready binary string extraction tool. The work spans 56 commits across 132 files (+7,963 / -6,477 lines), organized into five major workstreams.
610 tests pass (23 platform-skipped), zero clippy warnings, all pre-commit hooks green.
What Changed
1. Pipeline Architecture (New)
Introduced a modular pipeline orchestrator that replaces ad-hoc extraction logic:
Pipeline::new(config).run(&path)-- single entry point for all analysisPipelineConfigwith builder pattern for extraction, filtering, output, and debug optionsFilterEnginefor tag inclusion/exclusion, encoding, min-length, and top-N filteringmmap-guardintegration for safe memory-mapped file I/O (replaces rawmemmap2)Key files:
src/pipeline/mod.rs,src/pipeline/config.rs,src/pipeline/filter.rs,src/pipeline/normalizer.rs2. CLI Hardening
Complete rewrite of the CLI layer with production-quality UX:
-j(json),-m(min-len),-t(top),-e(enc).expect(), hidden when non-TTY or NO_COLOR)patharg::InputArgwith progress feedback--notagsrenamed to--no-tags(kebab-case consistency)--summary,--debug,--rawmodes with proper conflict declarationsKey files:
src/main.rs,src/types/error.rs3. Core Data Model Improvements
FoundString::new()+ builder methods replace struct literals (forward-compatible)StringContextconstructors for cleaner extraction codeSectionInfomade#[non_exhaustive]withwith_*buildersOutputMetadatawith analysis duration, top tag distribution, builder patternFilterContextwith builder methods for test ergonomicsExtractionConfig::with_min_length()builderKey files:
src/types/constructors.rs,src/types/found_string.rs,src/types/mod.rs4. Output Enhancements
section_weight,semantic_boost,noise_penalty) viaskip_serializing_ifdisplay_scorealways present in non-raw mode; forced to 0 in raw modeKey files:
src/output/table/plain.rs,src/output/table/tty.rs,src/output/json.rs5. Build & Infrastructure
strip = true,codegen-units = 1,lto = "thin"(release) /"fat"(dist)Test Plan
cargo clippy --all-targets -- -D warningscleancargo fmt --checkcleanNew Test Files in This PR
integration_flows_1_5.rsintegration_flows_6_7.rsintegration_flow8_errors.rsintegration_flow8_diagnostics.rsintegration_cli_errors.rsintegration_cli_coverage.rsintegration_cli_short_flags.rsintegration_flow1_minlen.rsintegration_flows_5_yara.rsBreaking Changes
--notagsrenamed to--no-tags(CLI flag)FoundStringstruct fields are no longer public for direct construction (useFoundString::new()+ builders)SectionInfois#[non_exhaustive](external crate consumers must use constructors)Risk Assessment
Risk Level: Medium
#![forbid(unsafe_code)]maintained, no new unsafeReviewer Notes
src/pipeline/) is entirely new -- start review thereGenerated with Claude Code