Contributor & AI-Agent Guidelines - Stringy#
This document is the definitive reference for anyone contributing to the Stringy repository — whether you are a human developer or an AI coding agent. It consolidates project standards from the .kiro/steering/ documents, Cursor IDE rules, and the codebase itself.
Critical Non-Negotiable Rules#
These rules are enforced by CI and toolchain configuration. Violating any of them causes a build or lint failure. There are no exceptions without explicit maintainer approval.
1. No unsafe Code#
All unsafe code is forbidden at the package level. This is enforced via [lints.rust] in Cargo.toml , not as a crate-level attribute:
[lints.rust]
unsafe_code = "forbid"
warnings = "deny"
Do not add unsafe blocks anywhere in the source tree. If a vetted dependency introduces unsafe internally (e.g., memmap2), that is acceptable — but no unsafe code may appear in Stringy's own Rust source files .
2. Zero Clippy Warnings#
All code must pass:
cargo clippy -- -D warnings
warnings = "deny" is enforced at the package level . Every lint violation is a hard build error in CI. The linting configuration in .cursor/rules/rust/linting-rules.mdc documents the intent behind each rule. AI assistants are explicitly prohibited from removing or weakening clippy restrictions without explicit maintainer approval .
Use just lint / just lint-rust for local checks .
3. ASCII-Only Source Code#
Do not use emoji, em-dashes (--), smart quotes, or any other non-ASCII characters in Rust source files. The non_ascii_literal lint is set to "warn" , which becomes an error under warnings = "deny".
Exception: Non-ASCII characters are allowed when the code is explicitly handling Unicode data (e.g., test strings for UTF-8 extraction like "Hello sekai" in test data literals) .
4. 500-Line File-Size Limit#
Keep source files under 500 lines. When a module grows beyond this, split it into a subdirectory with focused sub-modules . The extraction module, for example, uses extraction/mod.rs, extraction/ascii.rs, extraction/utf16.rs, etc. .
Note: extraction/mod.rs currently exceeds this limit as it contains extensive inline tests; the limit primarily applies to implementation code. Use #[cfg(test)] blocks at the bottom to segregate test code.
5. No Blanket #[allow] Without Justification#
Any use of #[allow(...)] must include an inline comment justifying why the lint is suppressed. Never apply #[allow] to an entire file or module :
// Correct -- targeted, justified suppression:
#[allow(clippy::collapsible_if)]
// Two conditions must be checked separately to avoid borrow checker issues
if current_string_bytes.len() >= min_length && current_string_bytes.len() <= max_length {
if let Some(start) = current_string_start { ... }
}
Blanket file-level #![allow(...)] attributes are not permitted .
Project Overview#
What Is Stringy?#
Stringy is a format-aware alternative to the Unix strings command. Instead of blindly scanning every byte for printable sequences, Stringy uses binary format intelligence -- knowledge of ELF, PE, and Mach-O structures -- to distinguish meaningful strings from binary noise .
Target use cases :
- Binary analysis and reverse engineering
- Malware analysis and triage
- YARA rule development
- Red team operations and tooling inspection
- General executable inspection
Technology Baseline#
| Property | Value |
|---|---|
| Language | Rust |
| Edition | 2024 |
| MSRV | 1.91+ |
| License | Apache-2.0 |
| Crate type | Both lib and bin |
Stringy is a synchronous, CLI-first tool. There is no async runtime. All processing is sequential and memory-conscious .
Data Flow Pipeline#
The extraction pipeline flows through these stages :
Input Binary File
|
v
Format Detection (goblin auto-detects ELF / PE / Mach-O)
|
v
Container Parsing (section headers, imports, exports, resources)
|
v
Targeted String Extraction (section-aware ASCII, UTF-8, UTF-16LE/BE scanning)
|
v
Encoding Detection & Conversion
|
v
Deduplication & Canonicalization
|
v
Semantic Classification (URL, domain, IP, path, GUID, Base64, etc.)
|
v
Symbol Processing (import/export names, Rust demangling)
|
v
Ranking Algorithm (section weight + semantic boost - noise penalty)
|
v
Output Formatter (JSONL / human-readable table / YARA)
|
v
Results
Format-first principle: Every extraction decision is informed by file format knowledge. A string in .rodata (weight 10.0) is treated as more meaningful than the same string in a code section (weight 1.0) .
Output Formats#
| Format | Description |
|---|---|
| JSONL | One JSON object per string, machine-readable |
| Human-readable | Sorted tables for terminal inspection |
| YARA | Strings formatted for direct use in YARA rules |
Module Structure#
The codebase follows a domain-driven layout under src/ :
src/
+-- main.rs # CLI entry point (clap Parser)
+-- lib.rs # Library root and public API re-exports
+-- types.rs # Core type definitions
+-- container/ # Binary format detection and parsing
| +-- mod.rs
| +-- elf.rs # ELF-specific parsing
| +-- pe.rs # PE-specific parsing
| +-- macho.rs # Mach-O-specific parsing
+-- extraction/ # String extraction algorithms
| +-- mod.rs # ExtractionConfig, StringExtractor trait, BasicExtractor
| +-- ascii.rs # ASCII/UTF-8 extraction
| +-- utf16.rs # UTF-16LE/BE extraction
| +-- pe_resources.rs # PE VERSIONINFO/STRINGTABLE/MANIFEST extraction
| +-- macho_load_commands.rs # Mach-O LC_LOAD_DYLIB / LC_RPATH extraction
| +-- filters.rs # CompositeNoiseFilter, NoiseFilter trait
| +-- config.rs # NoiseFilterConfig, FilterWeights
| +-- util.rs # UTF-16LE decoding utilities
+-- classification/ # Semantic analysis and tagging (types defined; impl pending)
| +-- mod.rs
| +-- semantic.rs # URL, domain, IP, path, GUID detection
| +-- symbols.rs # Import/export/symbol handling
| +-- ranking.rs # Scoring algorithm
+-- output/ # Output formatters (interfaces ready; impl pending)
+-- mod.rs
+-- json.rs # JSONL output
+-- human.rs # Human-readable tables
+-- yara.rs # YARA-friendly format
Module Responsibilities#
| Module | Status | Responsibility |
|---|---|---|
container/ | Complete | Binary format detection (ContainerParser trait), section metadata, import/export extraction |
extraction/ | ASCII + UTF-16 + PE resources complete | String extraction algorithms; StringExtractor trait; noise filtering |
classification/ | Types defined | Semantic tagging (Tag enum), symbol demangling, scoring |
output/ | Interfaces ready | JSONL, human-readable, YARA output formatters |
types.rs | Complete | FoundString, SectionInfo, Tag, BinaryFormat, StringyError, ContainerInfo, etc. |
Key Traits#
ContainerParser (src/container/) :
pub trait ContainerParser {
fn detect(data: &[u8]) -> bool;
fn parse(&self, data: &[u8]) -> Result<ContainerInfo>;
}
StringExtractor (src/extraction/mod.rs) :
pub trait StringExtractor {
fn extract(
&self,
data: &[u8],
container_info: &ContainerInfo,
config: &ExtractionConfig,
) -> Result<Vec<FoundString>>;
fn extract_from_section(
&self,
data: &[u8],
section: &SectionInfo,
config: &ExtractionConfig,
) -> Result<Vec<FoundString>>;
}
Key Dependencies#
| Crate | Purpose |
|---|---|
goblin 0.10 | Multi-format binary parser (ELF/PE/Mach-O) |
pelite 0.10 | PE resource extraction (VERSIONINFO, STRINGTABLE) |
clap 4.5 (derive) | CLI argument parsing |
serde + serde_json | Serialization / JSON output |
thiserror 2.0 | Structured error types |
entropy 0.4 | Shannon entropy for noise filtering |
insta | Snapshot testing |
criterion | Performance benchmarks |
Key Coding Patterns#
Section Weights (1.0-10.0 Float Range)#
The SectionInfo.weight: f32 field rates the likelihood of a section containing meaningful strings, directly influencing extraction priority and final string scoring. Section weights range from 1.0 to 10.0 .
ELF section weight assignments :
| Weight | Section(s) |
|---|---|
| 10.0 | .rodata, .rodata.str1.1, .rodata.str1.4, .rodata.str1.8 |
| 9.0 | .comment, .note, .note.gnu.build-id |
| 8.0 | Other StringData sections; Resources |
| 7.0 | ReadOnlyData sections |
| 5.0 | WritableData sections |
| 2.0 | Debug sections |
| 1.0 | Code and Other sections |
BasicExtractor sorts sections by the configured section_priority list first, then falls back to weight as a tiebreaker . The final FoundString.score: i32 integrates section weight with semantic boosts and noise penalties: Score = SectionWeight + SemanticBoost - NoisePenalty .
Error Handling with thiserror#
Use thiserror for all structured error types. Errors must include contextual information -- offsets, section names, and file paths -- to be actionable .
StringyError enum :
#[derive(Debug, thiserror::Error)]
pub enum StringyError {
#[error("Unsupported file format")]
UnsupportedFormat,
#[error("File I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Binary parsing error: {0}")]
ParseError(String), // Include section name and/or file path in the message
#[error("Invalid encoding in string at offset {offset}")]
EncodingError { offset: u64 }, // Named field carries the byte offset
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Memory mapping error: {0}")]
MemoryMapError(String),
}
From implementations exist for goblin::error::Error and pelite::Error , so ? propagation from those crates works transparently.
Rules:
- Use
?for propagation; avoidunwrap()in production code . - Continue processing other sections when one section fails -- partial results are better than no results .
- Include section names, offsets, and paths in
ParseError/EncodingErrormessages.
#[non_exhaustive] on Public Structs#
Public API types use #[non_exhaustive] to allow adding fields without breaking downstream code.
Rule: Always use Type::new() constructors for #[non_exhaustive] types. Never use struct literals.
ContainerInfo is #[non_exhaustive] and provides ContainerInfo::new() :
// Correct:
let info = ContainerInfo::new(format, sections, imports, exports, resources);
// Wrong -- will fail to compile if new fields are added:
// let info = ContainerInfo { format, sections, imports, exports, resources };
The Tag enum is also #[non_exhaustive] . Always include a wildcard arm when matching on Tag:
match tag {
Tag::Url => { /* ... */ }
Tag::FilePath => { /* ... */ }
_ => { /* required: catches new variants */ }
}
Test-Only Code Under #[cfg(test)]#
All test helpers, fixtures, and test-only data must be gated behind #[cfg(test)] . Use pub(crate) visibility for test helpers shared within the same crate.
#[cfg(test)]
mod tests {
use super::*;
// pub(crate) helpers go here if needed by other test modules
pub(crate) fn make_test_section() -> SectionInfo { ... }
#[test]
fn test_something() { ... }
}
The extraction module places 500+ lines of tests in a single #[cfg(test)] block at the end of the file -- this pattern is standard.
Idiomatic clap Derive Conventions#
Use #[derive(Parser)] on the CLI struct and #[command(...)] / #[arg(...)] for metadata :
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "stringy")]
#[command(about = "Extract meaningful strings from binary files")]
#[command(version)]
struct Cli {
/// Input binary file to analyze
#[arg(value_name = "FILE")]
input: PathBuf,
/// Minimum string length
#[arg(short = 'm', long, default_value_t = 4)]
min_len: usize,
}
CLI Flags Reference#
The following flags are specified in the design documents. Not all are yet implemented :
| Flag | Short | Type | Description |
|---|---|---|---|
FILE | -- | PathBuf | Input binary file (positional, required) |
--json | -j | bool | Output in JSONL format |
--yara | -- | bool | Output as YARA rule strings |
--only-tags | -- | Vec<String> | Show only strings matching these tags |
--no-tags | -- | bool | Suppress tag display in output |
--min-len | -m | usize | Minimum string length (default: 4) |
--top | -t | usize | Limit output to top N results |
--enc | -- | String | Encoding filter (ascii, utf8, utf16le, utf16be) |
--raw | -- | bool | Disable noise filtering, show all strings |
--summary | -- | bool | Show extraction summary/metadata |
--debug | -- | bool | Enable debug output |
LazyLock for Regex Compilation#
Compile all regex patterns exactly once at startup using std::sync::LazyLock. Never compile regexes inside hot paths .
use std::sync::LazyLock;
static URL_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"https?://[^\s]+").expect("URL regex is valid")
});
fn is_url(text: &str) -> bool {
URL_REGEX.is_match(text)
}
Use .expect("reason why this regex is valid") inside LazyLock::new(). Panicking at startup for a malformed static regex is correct behavior -- it surfaces the bug immediately and clearly.
Rustdoc for All Public APIs#
All public functions and types require comprehensive rustdoc with examples, error documentation, and caveats . See ContainerInfo in src/types.rs as the reference pattern.
Critical GOTCHAS#
Struct-Literal Update Checklists#
When you add a field to any core struct, update every construction site. The compiler enforces this for non-#[non_exhaustive] structs, but you must still audit the full list manually.
FoundString -- 10 fields :
| Field | Type |
|---|---|
text | String |
encoding | Encoding |
offset | u64 |
rva | Option<u64> |
section | Option<String> |
length | u32 |
tags | Vec<Tag> |
score | i32 |
source | StringSource |
confidence | f32 |
Construction sites to update: .
SectionInfo -- 8 fields :
| Field | Type |
|---|---|
name | String |
offset | u64 |
size | u64 |
rva | Option<u64> |
section_type | SectionType |
is_executable | bool |
is_writable | bool |
weight | f32 |
Construction sites to update: src/container/elf.rs , pe.rs, macho.rs, and all test SectionInfo literals in extraction/mod.rs and other test modules.
ContainerInfo -- 5 fields, #[non_exhaustive] :
Always use ContainerInfo::new() . Never write struct literals for this type. Construction sites are the parser parse() methods in elf.rs , pe.rs, and macho.rs.
ExtractionConfig -- 14 fields :
Fields: min_length, max_length, encodings, scan_code_sections, include_debug, section_priority, include_symbols, min_ascii_length, min_wide_length, enabled_encodings, noise_filtering_enabled, min_confidence_threshold, utf16_min_confidence, utf16_byte_order, utf16_confidence_threshold.
When adding a field: update Default::default() and all test sites that use ..Default::default() struct update syntax.
CLI Gotchas#
The current main.rs is a stub -- only the FILE positional argument exists . When implementing the full CLI:
--min-len/-m: needs a customusizerange parser;min_length == 0is rejected byExtractionConfig::validate(). Reject at the CLI layer, not silently.--only-tagscase-sensitivity:Tagserde renames use lowercase shorthand ("ipv4","ipv6","filepath","regpath","b64","fmt","dylib-path","rpath", etc.) -- not the Rust variant names . Parse using the serde rename values.- Exit code taxonomy: Define codes carefully for scripting use (e.g., 0=success with results, 1=no strings found, 2=usage error, 3=I/O or parse error).
--enc asciiis a content filter: It filters the output to ASCII-only strings. It does NOT disable UTF-16 scanning on binary input. This is intentional.
Dual Encoding Fields in ExtractionConfig#
ExtractionConfig has two encoding filter fields: encodings and enabled_encodings . BasicExtractor checks both with OR logic :
let ascii_enabled = config.encodings.contains(&Encoding::Ascii)
|| config.enabled_encodings.contains(&Encoding::Ascii);
Always check both fields. This pattern is repeated for UTF-8 and UTF-16 . Any new extractor must follow the same pattern.
mmap-Guard / memmap2 Unsafe Boundary#
memmap2 is planned for large-file support . When implemented:
- The
Mmapguard must outlive every&[u8]slice derived from it. - Files modified concurrently after mapping create TOCTOU hazards -- the mapped bytes can change under you.
- Wrap
Mmapin a guard struct that owns the map and exposes slices only through methods that enforce lifetime constraints. memmap2itself usesunsafeinternally; our code remainsunsafe-free by relying on its safe API.
Pipeline Non-Determinism#
Output ordering is not stable unless you explicitly sort:
BasicExtractorsorts sections bysection_prioritylist, then byweightas a fallback . Sections with equal priority are ordered by binary format iteration, which varies.- Import and export names are appended after section strings .
- For deterministic snapshot tests with
insta: sort results by(section, offset)orscorebefore asserting.
Raw mode behavior: When --raw is implemented, it bypasses the min_confidence_threshold filter . Expect significantly more noise -- thousands of low-quality strings from code sections. Test fixtures used for raw-mode tests must explicitly account for this.
FoundString: confidence vs. score Are Different#
FoundString carries two distinct quality fields :
| Field | Type | Range | Meaning |
|---|---|---|---|
confidence | f32 | 0.0-1.0 | Noise filter score. 1.0 = definitely legitimate. Below min_confidence_threshold = dropped. |
score | i32 | unbounded | Final ranking score. Combines section weight, semantic boosts, noise penalties. Used for --top N. |
Import and export names receive confidence = 1.0 but score = 0 -- ranking is not applied until the classification/ranking stage is implemented. Do not conflate these two fields.
Tag Enum Is #[non_exhaustive]#
Tag is #[non_exhaustive] . Every match on Tag must include _ => { }. Omitting the wildcard arm will cause a compile error when new variants are added.
Test Fixtures#
Integration test binaries (ELF, PE, Mach-O) live in tests/fixtures/ and are committed to the repository. Ensure fixtures are present before running integration tests. The design document specifies the expected fixture directory structure .
CI Quality Gate#
Always run just ci-check before marking work complete . This executes formatting, linting, compilation, tests, security audits, and license checks in sequence. Do not commit code that fails this gate.
Common development cycle commands :
just check # Fast type-check and lint
just fmt # Format all code (Rust + markdown)
just test # Run all tests
just lint-rust # Strict clippy with warnings-as-errors
just ci-check # Full CI validation gate