From 2dc2ca16a90e27f797e96221daa922f6a9e1d604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:20:27 +0000 Subject: [PATCH 01/13] Initial plan From 6868c2086b00a3133e52bce72cccf4c4a1bf9c89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:30:25 +0000 Subject: [PATCH 02/13] Enhance ELF symbol extraction with comprehensive symbol types and visibility filtering Co-authored-by: unclesp1d3r <251112+unclesp1d3r@users.noreply.github.com> --- src/container/elf.rs | 81 +++++++++++++++++++++++++++++++++++++++- tests/integration_elf.rs | 56 +++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/container/elf.rs b/src/container/elf.rs index 6529e41..fe0b0b0 100644 --- a/src/container/elf.rs +++ b/src/container/elf.rs @@ -90,12 +90,14 @@ impl ElfParser { // Import symbols are: // - Undefined (st_shndx == SHN_UNDEF) // - Global or weak binding - // - Functions or objects + // - Functions, objects, TLS variables, or IFuncs if sym.st_shndx == (goblin::elf::section_header::SHN_UNDEF as usize) && (sym.st_bind() == goblin::elf::sym::STB_GLOBAL || sym.st_bind() == goblin::elf::sym::STB_WEAK) && (sym.st_type() == goblin::elf::sym::STT_FUNC || sym.st_type() == goblin::elf::sym::STT_OBJECT + || sym.st_type() == goblin::elf::sym::STT_TLS + || sym.st_type() == goblin::elf::sym::STT_GNU_IFUNC || sym.st_type() == goblin::elf::sym::STT_NOTYPE) { if let Some(name) = elf.dynstrtab.get_at(sym.st_name) { @@ -122,6 +124,8 @@ impl ElfParser { || sym.st_bind() == goblin::elf::sym::STB_WEAK) && (sym.st_type() == goblin::elf::sym::STT_FUNC || sym.st_type() == goblin::elf::sym::STT_OBJECT + || sym.st_type() == goblin::elf::sym::STT_TLS + || sym.st_type() == goblin::elf::sym::STT_GNU_IFUNC || sym.st_type() == goblin::elf::sym::STT_NOTYPE) { if let Some(name) = elf.strtab.get_at(sym.st_name) { @@ -146,6 +150,25 @@ impl ElfParser { imports } + /// Extract DT_NEEDED entries (library dependencies) from ELF dynamic section + /// + /// This method is currently used in tests and reserved for future use when implementing + /// symbol-to-library mapping. ELF doesn't directly associate imported symbols with specific + /// libraries without analyzing version symbols or relocation tables, which requires more + /// sophisticated analysis than currently implemented. + #[allow(dead_code)] + fn extract_needed_libraries(&self, elf: &Elf) -> Vec { + if let Some(ref dynamic) = elf.dynamic { + dynamic + .get_libraries(&elf.dynstrtab) + .iter() + .map(|&s| s.to_string()) + .collect() + } else { + Vec::new() + } + } + /// Attempt to extract library information from DT_NEEDED entries /// This is a best-effort approach since ELF doesn't directly link symbols to libraries fn extract_library_from_needed(&self, elf: &Elf, _symbol_name: &str) -> Option { @@ -168,10 +191,17 @@ impl ElfParser { // Extract from dynamic symbol table for sym in &elf.dynsyms { + // Export symbols must be: + // - Defined (not SHN_UNDEF) + // - Global or weak binding + // - Visible (not hidden or internal) + // - Have a valid address if (sym.st_bind() == goblin::elf::sym::STB_GLOBAL || sym.st_bind() == goblin::elf::sym::STB_WEAK) && sym.st_shndx != (goblin::elf::section_header::SHN_UNDEF as usize) && sym.st_value != 0 + && sym.st_visibility() != goblin::elf::sym::STV_HIDDEN + && sym.st_visibility() != goblin::elf::sym::STV_INTERNAL { if let Some(name) = elf.dynstrtab.get_at(sym.st_name) { if !name.is_empty() && seen_names.insert(name.to_string()) { @@ -191,8 +221,12 @@ impl ElfParser { || sym.st_bind() == goblin::elf::sym::STB_WEAK) && sym.st_shndx != (goblin::elf::section_header::SHN_UNDEF as usize) && sym.st_value != 0 + && sym.st_visibility() != goblin::elf::sym::STV_HIDDEN + && sym.st_visibility() != goblin::elf::sym::STV_INTERNAL && (sym.st_type() == goblin::elf::sym::STT_FUNC || sym.st_type() == goblin::elf::sym::STT_OBJECT + || sym.st_type() == goblin::elf::sym::STT_TLS + || sym.st_type() == goblin::elf::sym::STT_GNU_IFUNC || sym.st_type() == goblin::elf::sym::STT_NOTYPE) { if let Some(name) = elf.strtab.get_at(sym.st_name) { @@ -471,4 +505,49 @@ mod tests { // Verify the parser exists let _ = parser; } + + #[test] + fn test_extract_needed_libraries_with_test_binary() { + // Test library extraction with the current test binary + // This test demonstrates the extract_needed_libraries method works with real ELF files + let current_exe = std::env::current_exe().expect("Failed to get current executable"); + + if let Ok(data) = std::fs::read(¤t_exe) { + if let Ok(goblin::Object::Elf(elf)) = goblin::Object::parse(&data) { + let parser = ElfParser::new(); + let libraries = parser.extract_needed_libraries(&elf); + + // The test binary should have some libraries (e.g., libc) unless statically linked + println!("Test binary libraries: {:?}", libraries); + + // Just verify the method runs without panicking + // Actual library content depends on the build environment + } + } + } + + #[test] + fn test_symbol_type_constants() { + // Test additional symbol type constants we're now using + use goblin::elf::sym::{STT_GNU_IFUNC, STT_TLS}; + + // Verify the constants we're now using in import/export filtering + assert_eq!(STT_TLS, 6); // Thread-local storage + assert_eq!(STT_GNU_IFUNC, 10); // Indirect function + + // These constants are used in our enhanced import/export filtering logic + } + + #[test] + fn test_symbol_visibility_constants() { + // Test symbol visibility constants + use goblin::elf::sym::{STV_DEFAULT, STV_HIDDEN, STV_INTERNAL}; + + // Verify the visibility constants we're using for filtering + assert_eq!(STV_DEFAULT, 0); + assert_eq!(STV_HIDDEN, 2); + assert_eq!(STV_INTERNAL, 1); + + // These constants are used to filter out hidden and internal symbols from exports + } } diff --git a/tests/integration_elf.rs b/tests/integration_elf.rs index 28dc765..2ab6724 100644 --- a/tests/integration_elf.rs +++ b/tests/integration_elf.rs @@ -335,3 +335,59 @@ fn test_elf_section_classification_integration() { } } } + +#[test] +#[cfg(target_family = "unix")] +fn test_elf_library_dependencies() { + // Test with the current binary (this test executable) which should have library dependencies + let current_exe = std::env::current_exe().expect("Failed to get current executable path"); + + if let Ok(elf_data) = fs::read(¤t_exe) { + // Parse with goblin to check if it's ELF + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(elf)) => { + // Check if we have a dynamic section + if let Some(ref dynamic) = elf.dynamic { + // Extract libraries using the method we're testing + let libraries = dynamic.get_libraries(&elf.dynstrtab); + + println!("Found {} library dependencies:", libraries.len()); + for lib in &libraries { + println!(" - {}", lib); + } + + // A dynamically linked ELF binary should typically have at least one library + // (e.g., libc.so.6 on Linux) + // But we'll be lenient here since we might be on a different platform + if !libraries.is_empty() { + // Verify at least one common library is present + let has_libc = libraries.iter().any(|lib| lib.contains("libc")); + let has_libpthread = + libraries.iter().any(|lib| lib.contains("pthread")); + let has_libm = libraries.iter().any(|lib| lib.contains("libm")); + + // At least one common library should be present in a typical executable + if has_libc || has_libpthread || has_libm { + println!("✓ Found expected library dependencies"); + } + } else { + println!( + "No library dependencies found. This might be a static binary or on a non-Linux platform." + ); + } + } else { + println!("No dynamic section found. This might be a static binary."); + } + } + Ok(goblin::Object::Mach(_)) => { + println!("Got Mach-O binary (expected on macOS), skipping ELF library test"); + } + Ok(_) => { + println!("Got non-ELF binary format, skipping test"); + } + Err(e) => { + println!("Failed to parse binary: {}, skipping test", e); + } + } + } +} From 9cdc32074c99a8777af5e4b22e6701ef104335c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:37:55 +0000 Subject: [PATCH 03/13] Add insta snapshot tests for ELF symbol extraction Co-authored-by: unclesp1d3r <251112+unclesp1d3r@users.noreply.github.com> --- tests/integration_elf.rs | 73 +++++++++++++++++++ ...ntegration_elf__elf_symbol_extraction.snap | 63 ++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 tests/snapshots/integration_elf__elf_symbol_extraction.snap diff --git a/tests/integration_elf.rs b/tests/integration_elf.rs index 2ab6724..cf92f80 100644 --- a/tests/integration_elf.rs +++ b/tests/integration_elf.rs @@ -4,6 +4,7 @@ use std::io::Write; use std::process::Command; use stringy::container::{ContainerParser, ElfParser}; use tempfile::TempDir; +use insta::assert_snapshot; #[test] #[cfg(target_family = "unix")] @@ -391,3 +392,75 @@ fn test_elf_library_dependencies() { } } } + +#[test] +#[cfg(target_family = "unix")] +fn test_elf_symbol_extraction_snapshot() { + // Test with the current binary to create a snapshot of symbol extraction + let current_exe = std::env::current_exe().expect("Failed to get current executable path"); + + if let Ok(elf_data) = fs::read(¤t_exe) { + if ElfParser::detect(&elf_data) { + let parser = ElfParser::new(); + if let Ok(container_info) = parser.parse(&elf_data) { + // Create a formatted output for snapshot testing + let mut output = String::new(); + + // Document imports + output.push_str("=== IMPORTS ===\n"); + output.push_str(&format!("Total: {}\n\n", container_info.imports.len())); + + // Take first 10 imports for snapshot (to keep it manageable) + for (i, import) in container_info.imports.iter().take(10).enumerate() { + output.push_str(&format!( + "Import {}: {}\n", + i + 1, + import.name + )); + if let Some(ref lib) = import.library { + output.push_str(&format!(" Library: {}\n", lib)); + } + if let Some(addr) = import.address { + output.push_str(&format!(" Address: 0x{:x}\n", addr)); + } + output.push('\n'); + } + + if container_info.imports.len() > 10 { + output.push_str(&format!( + "... and {} more imports\n\n", + container_info.imports.len() - 10 + )); + } + + // Document exports + output.push_str("=== EXPORTS ===\n"); + output.push_str(&format!("Total: {}\n\n", container_info.exports.len())); + + // Take first 10 exports for snapshot + for (i, export) in container_info.exports.iter().take(10).enumerate() { + output.push_str(&format!( + "Export {}: {}\n", + i + 1, + export.name + )); + output.push_str(&format!(" Address: 0x{:x}\n", export.address)); + if let Some(ord) = export.ordinal { + output.push_str(&format!(" Ordinal: {}\n", ord)); + } + output.push('\n'); + } + + if container_info.exports.len() > 10 { + output.push_str(&format!( + "... and {} more exports\n", + container_info.exports.len() - 10 + )); + } + + // Snapshot the output + assert_snapshot!("elf_symbol_extraction", output); + } + } + } +} diff --git a/tests/snapshots/integration_elf__elf_symbol_extraction.snap b/tests/snapshots/integration_elf__elf_symbol_extraction.snap new file mode 100644 index 0000000..8769a43 --- /dev/null +++ b/tests/snapshots/integration_elf__elf_symbol_extraction.snap @@ -0,0 +1,63 @@ +--- +source: tests/integration_elf.rs +expression: output +--- +=== IMPORTS === +Total: 134 + +Import 1: __libc_start_main + +Import 2: __gmon_start__ + +Import 3: _ITM_deregisterTMCloneTable + +Import 4: _ITM_registerTMCloneTable + +Import 5: __cxa_finalize + +Import 6: _Unwind_Resume + +Import 7: memcpy + +Import 8: memset + +Import 9: memcmp + +Import 10: bcmp + +... and 124 more imports + +=== EXPORTS === +Total: 7362 + +Export 1: _start + Address: 0xed9e0 + +Export 2: main + Address: 0xf7f90 + +Export 3: _RNvCsj3IbkTTFM3W_7___rustc10rust_panic + Address: 0x363330 + +Export 4: _RNvCsj3IbkTTFM3W_7___rustc17___rust_drop_panic + Address: 0x3634d0 + +Export 5: _RNvCsj3IbkTTFM3W_7___rustc17rust_begin_unwind + Address: 0x3635a0 + +Export 6: _RNvCsj3IbkTTFM3W_7___rustc24___rust_foreign_exception + Address: 0x363650 + +Export 7: _ZN3std3sys4args4unix3imp15ARGV_INIT_ARRAY17h53e3ae54f15b3a00E + Address: 0x3c3030 + +Export 8: rust_eh_personality + Address: 0x3a8060 + +Export 9: _RNvCsj3IbkTTFM3W_7___rustc18___rust_start_panic + Address: 0x3a8620 + +Export 10: _RNvCsj3IbkTTFM3W_7___rustc20___rust_panic_cleanup + Address: 0x3a86d0 + +... and 7352 more exports From f2eaebc3540e32123e1d62ad8328241c804a44b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:40:01 +0000 Subject: [PATCH 04/13] Update documentation for enhanced ELF symbol extraction Co-authored-by: unclesp1d3r <251112+unclesp1d3r@users.noreply.github.com> --- docs/src/binary-formats.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/src/binary-formats.md b/docs/src/binary-formats.md index c65d698..ccf5a8a 100644 --- a/docs/src/binary-formats.md +++ b/docs/src/binary-formats.md @@ -22,6 +22,27 @@ Used primarily on Linux and other Unix-like systems. - **Dynamic Strings**: Process `.dynstr` for library names and symbols - **Section Flags**: Use `SHF_EXECINSTR` and `SHF_WRITE` for classification - **Virtual Addresses**: Map file offsets to runtime addresses +- **Dynamic Linking**: Parse `DT_NEEDED` entries to extract library dependencies +- **Symbol Types**: Support for functions (STT_FUNC), objects (STT_OBJECT), TLS variables (STT_TLS), and indirect functions (STT_GNU_IFUNC) +- **Symbol Visibility**: Filter hidden and internal symbols from exports (STV_HIDDEN, STV_INTERNAL) + +### Enhanced Symbol Extraction + +The ELF parser now provides comprehensive symbol extraction with: + +1. **Import Detection**: Identifies all undefined symbols (SHN_UNDEF) that need runtime resolution + - Supports multiple symbol types: functions, objects, TLS variables, and indirect functions + - Handles both global and weak bindings + - Foundation for library mapping through DT_NEEDED analysis + +2. **Export Detection**: Extracts all globally visible defined symbols + - Filters out hidden (STV_HIDDEN) and internal (STV_INTERNAL) symbols + - Includes both strong and weak symbols + - Supports all relevant symbol types + +3. **Library Dependencies**: Extracts DT_NEEDED entries from the dynamic section + - Provides list of required shared libraries + - Foundation for future symbol-to-library mapping ### Implementation Details @@ -40,6 +61,23 @@ impl ElfParser { // ... more classifications } } + + fn extract_imports(&self, elf: &Elf) -> Vec { + // Extract undefined symbols from dynamic symbol table + // Supports STT_FUNC, STT_OBJECT, STT_TLS, STT_GNU_IFUNC, STT_NOTYPE + // Handles both STB_GLOBAL and STB_WEAK bindings + } + + fn extract_exports(&self, elf: &Elf) -> Vec { + // Extract defined symbols with global/weak binding + // Filters out STV_HIDDEN and STV_INTERNAL symbols + // Includes all relevant symbol types + } + + fn extract_needed_libraries(&self, elf: &Elf) -> Vec { + // Parse DT_NEEDED entries from dynamic section + // Returns list of required shared library names + } } ``` From 6dce04e25171da54ab3005be85384d79c87c079b Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 20:51:11 -0500 Subject: [PATCH 05/13] Update deny.toml to include additional allowed licenses and refine dependency management settings - Added "CC0-1.0" and "Unlicense" to the list of allowed licenses. - Cleared the skip-tree section for better clarity. - Introduced new configuration for allowing specific organizations for git sources. Signed-off-by: UncleSp1d3r --- deny.toml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/deny.toml b/deny.toml index 56fc895..7c1ba4c 100644 --- a/deny.toml +++ b/deny.toml @@ -18,10 +18,12 @@ unused-allowed-license = "allow" allow = [ "Apache-2.0", "Apache-2.0 WITH LLVM-exception", + "CC0-1.0", "MIT", "BSD-3-Clause", "ISC", "Unicode-3.0", + "Unlicense", "Zlib", ] include-dev = true @@ -38,12 +40,9 @@ deny = [ { crate = "openssl-sys", use-instead = "rustls" }, "libssh2-sys", { crate = "cmake", use-instead = "cc" }, - { crate = "windows", reason = "bloated and unnecessary", use-instead = "ideally inline bindings, practically, windows-sys" }, ] skip = [] -skip-tree = [ - { crate = "windows-sys", reason = "a foundational crate for many that bumps far too frequently to ever have a shared version" }, -] +skip-tree = [] [advisories] @@ -55,3 +54,10 @@ ignore = [] # Allow crates from crates.io unknown-registry = "deny" unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] + +[sources.allow-org] +# Allow specific organizations for git sources if needed +github = [] +gitlab = [] +bitbucket = [] From 8143c47c63df89937273e778139da72fa85a0dab Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 20:52:16 -0500 Subject: [PATCH 06/13] Enhance ELF symbol extraction documentation and improve code clarity - Updated the documentation for enhanced symbol extraction, detailing import/export detection and library dependencies. - Refined comments in the ELF parser code for better clarity and future use. - Cleaned up whitespace in test cases to improve readability. Signed-off-by: UncleSp1d3r --- docs/src/binary-formats.md | 3 +++ src/container/elf.rs | 8 ++++---- tests/integration_elf.rs | 31 +++++++++++-------------------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/docs/src/binary-formats.md b/docs/src/binary-formats.md index ccf5a8a..ad35471 100644 --- a/docs/src/binary-formats.md +++ b/docs/src/binary-formats.md @@ -31,16 +31,19 @@ Used primarily on Linux and other Unix-like systems. The ELF parser now provides comprehensive symbol extraction with: 1. **Import Detection**: Identifies all undefined symbols (SHN_UNDEF) that need runtime resolution + - Supports multiple symbol types: functions, objects, TLS variables, and indirect functions - Handles both global and weak bindings - Foundation for library mapping through DT_NEEDED analysis 2. **Export Detection**: Extracts all globally visible defined symbols + - Filters out hidden (STV_HIDDEN) and internal (STV_INTERNAL) symbols - Includes both strong and weak symbols - Supports all relevant symbol types 3. **Library Dependencies**: Extracts DT_NEEDED entries from the dynamic section + - Provides list of required shared libraries - Foundation for future symbol-to-library mapping diff --git a/src/container/elf.rs b/src/container/elf.rs index fe0b0b0..550653b 100644 --- a/src/container/elf.rs +++ b/src/container/elf.rs @@ -151,7 +151,7 @@ impl ElfParser { } /// Extract DT_NEEDED entries (library dependencies) from ELF dynamic section - /// + /// /// This method is currently used in tests and reserved for future use when implementing /// symbol-to-library mapping. ELF doesn't directly associate imported symbols with specific /// libraries without analyzing version symbols or relocation tables, which requires more @@ -511,15 +511,15 @@ mod tests { // Test library extraction with the current test binary // This test demonstrates the extract_needed_libraries method works with real ELF files let current_exe = std::env::current_exe().expect("Failed to get current executable"); - + if let Ok(data) = std::fs::read(¤t_exe) { if let Ok(goblin::Object::Elf(elf)) = goblin::Object::parse(&data) { let parser = ElfParser::new(); let libraries = parser.extract_needed_libraries(&elf); - + // The test binary should have some libraries (e.g., libc) unless statically linked println!("Test binary libraries: {:?}", libraries); - + // Just verify the method runs without panicking // Actual library content depends on the build environment } diff --git a/tests/integration_elf.rs b/tests/integration_elf.rs index cf92f80..02ed6a5 100644 --- a/tests/integration_elf.rs +++ b/tests/integration_elf.rs @@ -1,10 +1,10 @@ +use insta::assert_snapshot; use std::fs; use std::fs::File; use std::io::Write; use std::process::Command; use stringy::container::{ContainerParser, ElfParser}; use tempfile::TempDir; -use insta::assert_snapshot; #[test] #[cfg(target_family = "unix")] @@ -363,8 +363,7 @@ fn test_elf_library_dependencies() { if !libraries.is_empty() { // Verify at least one common library is present let has_libc = libraries.iter().any(|lib| lib.contains("libc")); - let has_libpthread = - libraries.iter().any(|lib| lib.contains("pthread")); + let has_libpthread = libraries.iter().any(|lib| lib.contains("pthread")); let has_libm = libraries.iter().any(|lib| lib.contains("libm")); // At least one common library should be present in a typical executable @@ -405,18 +404,14 @@ fn test_elf_symbol_extraction_snapshot() { if let Ok(container_info) = parser.parse(&elf_data) { // Create a formatted output for snapshot testing let mut output = String::new(); - + // Document imports output.push_str("=== IMPORTS ===\n"); output.push_str(&format!("Total: {}\n\n", container_info.imports.len())); - + // Take first 10 imports for snapshot (to keep it manageable) for (i, import) in container_info.imports.iter().take(10).enumerate() { - output.push_str(&format!( - "Import {}: {}\n", - i + 1, - import.name - )); + output.push_str(&format!("Import {}: {}\n", i + 1, import.name)); if let Some(ref lib) = import.library { output.push_str(&format!(" Library: {}\n", lib)); } @@ -425,39 +420,35 @@ fn test_elf_symbol_extraction_snapshot() { } output.push('\n'); } - + if container_info.imports.len() > 10 { output.push_str(&format!( "... and {} more imports\n\n", container_info.imports.len() - 10 )); } - + // Document exports output.push_str("=== EXPORTS ===\n"); output.push_str(&format!("Total: {}\n\n", container_info.exports.len())); - + // Take first 10 exports for snapshot for (i, export) in container_info.exports.iter().take(10).enumerate() { - output.push_str(&format!( - "Export {}: {}\n", - i + 1, - export.name - )); + output.push_str(&format!("Export {}: {}\n", i + 1, export.name)); output.push_str(&format!(" Address: 0x{:x}\n", export.address)); if let Some(ord) = export.ordinal { output.push_str(&format!(" Ordinal: {}\n", ord)); } output.push('\n'); } - + if container_info.exports.len() > 10 { output.push_str(&format!( "... and {} more exports\n", container_info.exports.len() - 10 )); } - + // Snapshot the output assert_snapshot!("elf_symbol_extraction", output); } From 081a1522f07488477175c4f4c36cf9a2f7b5c605 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 21:05:39 -0500 Subject: [PATCH 07/13] Add Rust coding standards, error handling patterns, and performance optimization guidelines - Introduced comprehensive documentation for Rust coding standards, emphasizing the use of Rust 2024 Edition, zero warnings policy, and structured error handling with `thiserror`. - Established error handling patterns detailing structured error types, context, propagation, and recovery strategies. - Added performance optimization standards focusing on high-performance binary processing, memory management, and benchmarking practices using Criterion. Signed-off-by: UncleSp1d3r --- .cursor/rules/rust/cargo-toml.mdc | 81 ++++ .../rules/rust/configuration-management.mdc | 80 ++++ .../rules/rust/error-handling-patterns.mdc | 249 ++++++++++ .cursor/rules/rust/error-handling.mdc | 62 +++ .cursor/rules/rust/linting-rules.mdc | 452 ++++++++++++++++++ .../rules/rust/performance-optimization.mdc | 275 +++++++++++ .cursor/rules/rust/rust-standards.mdc | 42 ++ 7 files changed, 1241 insertions(+) create mode 100644 .cursor/rules/rust/cargo-toml.mdc create mode 100644 .cursor/rules/rust/configuration-management.mdc create mode 100644 .cursor/rules/rust/error-handling-patterns.mdc create mode 100644 .cursor/rules/rust/error-handling.mdc create mode 100644 .cursor/rules/rust/linting-rules.mdc create mode 100644 .cursor/rules/rust/performance-optimization.mdc create mode 100644 .cursor/rules/rust/rust-standards.mdc diff --git a/.cursor/rules/rust/cargo-toml.mdc b/.cursor/rules/rust/cargo-toml.mdc new file mode 100644 index 0000000..3866bdb --- /dev/null +++ b/.cursor/rules/rust/cargo-toml.mdc @@ -0,0 +1,81 @@ +--- +globs: Cargo.toml +--- + +# Cargo.toml Standards for Stringy + +## Package Configuration + +- Use **Rust 2024 Edition** (MSRV: 1.91+) as specified in the package +- Single crate structure (not a workspace) +- Enforce lint policy via `[lints.rust]` configuration + - Forbid unsafe code globally + - Deny all warnings to preserve code quality + +Example `Cargo.toml` structure: + +```toml +[package] +name = "stringy" +version = "0.1.0" +edition = "2024" +authors = ["UncleSp1d3r "] +description = "A smarter alternative to the strings command that leverages format-specific knowledge" +license = "Apache-2.0" +repository = "https://github.com/EvilBit-Labs/StringyMcStringFace" +homepage = "http://evilbitlabs.io/StringyMcStringFace/" +keywords = ["binary", "strings", "analysis", "reverse-engineering", "malware"] +categories = ["command-line-utilities", "development-tools"] + +[lib] +name = "stringy" +path = "src/lib.rs" + +[[bin]] +name = "stringy" +path = "src/main.rs" + +[lints.rust] +unsafe_code = "forbid" +warnings = "deny" +``` + +## Dependencies + +- **Core dependencies**: + - `clap = { version = "4.5", features = ["derive"] }` - CLI argument parsing + - `goblin = "0.10"` - Binary format parsing (ELF, PE, Mach-O) + - `serde = { version = "1.0", features = ["derive"] }` - Serialization + - `serde_json = "1.0"` - JSON output + - `thiserror = "2.0"` - Structured error types + +- **Dev dependencies**: + - `criterion = "0.7"` - Benchmarking + - `insta = "1.0"` - Snapshot testing + - `tempfile = "3.8"` - Temporary file handling in tests + +## Build Profiles + +- Use `[profile.dist]` for distribution builds with LTO: + +```toml +[profile.dist] +inherits = "release" +lto = "thin" +``` + +## Benchmarks + +- Define benchmarks in `[[bench]]` sections: + +```toml +[[bench]] +name = "elf" +harness = false +``` + +## Package Metadata + +- Include proper license (Apache-2.0) +- Provide clear description for binary analysis tool +- Include relevant keywords for discoverability diff --git a/.cursor/rules/rust/configuration-management.mdc b/.cursor/rules/rust/configuration-management.mdc new file mode 100644 index 0000000..521d507 --- /dev/null +++ b/.cursor/rules/rust/configuration-management.mdc @@ -0,0 +1,80 @@ +--- +globs: **/config*.rs,**/*config*.rs +alwaysApply: false +--- + +# Configuration Management Standards for Stringy + +## Configuration Architecture + +Stringy uses **CLI arguments only** for configuration via `clap`: + +- **Command-line flags** (only source of configuration) +- **No configuration files** - all options specified via CLI +- **No environment variables** - use CLI flags instead +- **No hierarchical configuration** - simple argument parsing + +## CLI Configuration + +Define CLI arguments using `clap` with derive macros: + +```rust +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "stringy")] +#[command(about = "Extract meaningful strings from binary files")] +struct Cli { + /// Input binary file to analyze + #[arg(value_name = "FILE")] + input: PathBuf, + + /// Minimum string length + #[arg(short, long, default_value_t = 4)] + min_len: usize, + + /// Output format (json, text, yara) + #[arg(short, long, default_value = "text")] + format: String, + + /// Only extract strings matching specific tags + #[arg(long)] + only: Option>, +} +``` + +## Configuration Validation + +Validate CLI arguments: + +```rust +impl Cli { + pub fn validate(&self) -> Result<(), StringyError> { + if self.min_len < 1 { + return Err(StringyError::ConfigError( + "Minimum string length must be at least 1".to_string() + )); + } + + let valid_formats = ["json", "text", "yara"]; + if !valid_formats.contains(&self.format.as_str()) { + return Err(StringyError::ConfigError( + format!("Invalid output format: {}. Must be one of: {:?}", + self.format, valid_formats) + )); + } + + Ok(()) + } +} +``` + +## No File-Based Configuration + +Stringy intentionally does not support configuration files to keep it simple and portable: + +- All configuration comes from CLI arguments +- No need to manage config file locations +- Works the same way across all environments +- Easier to use in scripts and pipelines diff --git a/.cursor/rules/rust/error-handling-patterns.mdc b/.cursor/rules/rust/error-handling-patterns.mdc new file mode 100644 index 0000000..d5f8778 --- /dev/null +++ b/.cursor/rules/rust/error-handling-patterns.mdc @@ -0,0 +1,249 @@ +--- +globs: **/*.rs +alwaysApply: false +--- + +# Error Handling Patterns for Stringy + +## Error Handling Philosophy + +Stringy uses structured error handling with clear error boundaries: + +- **Structured Errors**: Use `thiserror` for all error types with derive macros +- **Error Context**: Provide detailed context in error messages (offsets, section names, file paths) +- **Error Boundaries**: Implement proper error boundaries for different components (parsing, extraction, classification) +- **Recovery Strategies**: Continue processing when possible, provide partial results + +## Error Type Definition + +Define structured error types using thiserror: + +```rust +use thiserror::Error; + +#[derive(Debug, 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), + + #[error("Invalid encoding in string at offset {offset}")] + EncodingError { offset: u64 }, + + #[error("Configuration error: {0}")] + ConfigError(String), + + #[error("Memory mapping error: {0}")] + MemoryMapError(String), +} + +// Convert from goblin errors +impl From for StringyError { + fn from(err: goblin::error::Error) -> Self { + StringyError::ParseError(err.to_string()) + } +} +``` + +## Error Context + +Provide detailed error context: + +```rust +fn parse_elf_section(data: &[u8], section_name: &str) -> Result { + let section = goblin::elf::Elf::parse(data) + .map_err(|e| StringyError::ParseError( + format!("Failed to parse ELF section '{}': {}", section_name, e) + ))?; + + // ... parsing logic + Ok(section_info) +} +``` + +## Component-Specific Error Handling + +Implement error boundaries for different components: + +```rust +// Container parsing errors +#[derive(Debug, Error)] +pub enum ContainerError { + #[error("Failed to detect binary format")] + FormatDetectionFailed, + + #[error("Unsupported binary format: {format:?}")] + UnsupportedFormat { format: BinaryFormat }, + + #[error("Section parsing failed: {section}: {error}")] + SectionParseFailed { section: String, error: String }, +} + +// String extraction errors +#[derive(Debug, Error)] +pub enum ExtractionError { + #[error("Invalid encoding at offset {offset}")] + InvalidEncoding { offset: u64 }, + + #[error("String extraction failed: {0}")] + ExtractionFailed(String), +} + +// Classification errors +#[derive(Debug, Error)] +pub enum ClassificationError { + #[error("Tagging failed: {0}")] + TaggingFailed(String), +} +``` + +## Error Recovery Patterns + +Implement graceful degradation: + +```rust +fn extract_strings_from_sections( + data: &[u8], + sections: &[SectionInfo] +) -> Vec { + let mut found_strings = Vec::new(); + + for section in sections { + match extract_strings_from_section(data, section) { + Ok(strings) => found_strings.extend(strings), + Err(e) => { + eprintln!("Warning: Failed to extract strings from section '{}': {}", + section.name, e); + // Continue with other sections + } + } + } + + found_strings +} +``` + +## Error Logging + +Implement structured error logging: + +```rust +fn handle_parsing_error(error: StringyError, context: &str) { + match error { + StringyError::UnsupportedFormat => { + eprintln!("Error: {} - Unsupported binary format", context); + } + StringyError::ParseError(msg) => { + eprintln!("Error: {} - Parse error: {}", context, msg); + } + StringyError::EncodingError { offset } => { + eprintln!("Error: {} - Invalid encoding at offset 0x{:x}", context, offset); + } + StringyError::IoError(e) => { + eprintln!("Error: {} - I/O error: {}", context, e); + } + _ => { + eprintln!("Error: {} - {}", context, error); + } + } +} +``` + +## Error Propagation + +Use proper error propagation patterns: + +```rust +// Use ? operator for early returns +fn parse_binary_file(path: &Path) -> Result { + let data = std::fs::read(path) + .map_err(|e| StringyError::IoError(e))?; + + let format = detect_format(&data); + let parser = create_parser(format)?; + let container_info = parser.parse(&data)?; + + Ok(container_info) +} + +// Use map_err for error transformation +fn extract_from_section(data: &[u8], section: &SectionInfo) -> Result> { + let section_data = &data[section.offset as usize..(section.offset + section.size) as usize]; + + extract_strings(section_data) + .map_err(|e| StringyError::ExtractionError { + section: section.name.clone(), + error: e.to_string(), + }) +} +``` + +## Error Testing + +Test error conditions thoroughly: + +```rust +#[test] +fn test_unsupported_format() { + let data = b"NOT_A_BINARY_FORMAT"; + let format = detect_format(data); + assert_eq!(format, BinaryFormat::Unknown); + + let result = create_parser(format); + assert!(matches!(result, Err(StringyError::UnsupportedFormat))); +} + +#[test] +fn test_malformed_binary() { + let data = b"\x7fELF\x01\x01\x01"; // Invalid ELF header + let parser = ElfParser::new(); + let result = parser.parse(data); + + assert!(result.is_err()); + if let Err(StringyError::ParseError(_)) = result { + // Expected + } else { + panic!("Expected ParseError"); + } +} + +#[test] +fn test_error_recovery() { + // Test that errors in one section don't stop processing of others + let result = extract_strings_from_sections(&data, §ions); + // Should return partial results even if some sections fail + assert!(!result.is_empty()); +} +``` + +## Error Documentation + +Document error conditions in rustdoc: + +```rust +/// Parses a binary file and extracts container information. +/// +/// # Errors +/// +/// Returns [`StringyError`] for: +/// - Unsupported binary formats +/// - I/O errors reading the file +/// - Binary parsing errors (malformed headers, invalid structures) +/// +/// # Examples +/// +/// ```rust,no_run +/// use stringy::container::parse_binary_file; +/// +/// let info = parse_binary_file("binary.exe")?; +/// println!("Format: {:?}", info.format); +/// ``` +pub fn parse_binary_file(path: &Path) -> Result { + // Implementation +} +``` diff --git a/.cursor/rules/rust/error-handling.mdc b/.cursor/rules/rust/error-handling.mdc new file mode 100644 index 0000000..4a5cb5e --- /dev/null +++ b/.cursor/rules/rust/error-handling.mdc @@ -0,0 +1,62 @@ +--- +globs: **/*.rs +--- + +# Error Handling Standards for Stringy + +## Error Types + +Use `thiserror` for structured error types: + +```rust +#[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), + + #[error("Invalid encoding in string at offset {offset}")] + EncodingError { offset: u64 }, + + #[error("Configuration error: {0}")] + ConfigError(String), + + #[error("Memory mapping error: {0}")] + MemoryMapError(String), +} +``` + +## Error Context + +- Provide detailed error messages with actionable suggestions +- Include relevant context information (file paths, offsets, section names, etc.) +- Convert `goblin` errors to `StringyError` using `From` implementations + +## Error Propagation + +- Use `?` operator for error propagation +- Convert between error types using `From` implementations +- Avoid `unwrap()` and `expect()` in production code + +## Binary Parsing Errors + +- Handle malformed binary files gracefully +- Provide clear error messages indicating what went wrong +- Include file offset information when available + +## Error Recovery + +- Continue processing other sections when one section fails +- Provide partial results when possible +- Log errors but don't crash on non-critical failures + +## Error Testing + +- Test all error conditions (invalid formats, malformed binaries, I/O errors) +- Validate error messages and context +- Test error propagation through the parsing pipeline diff --git a/.cursor/rules/rust/linting-rules.mdc b/.cursor/rules/rust/linting-rules.mdc new file mode 100644 index 0000000..a0c6cc8 --- /dev/null +++ b/.cursor/rules/rust/linting-rules.mdc @@ -0,0 +1,452 @@ +--- +globs: **/*.rs +--- + +# Rust Linting Rules for Stringy + +This document explains the intent behind each clippy lint rule in Stringy's Cargo.toml. These rules are carefully chosen for a binary analysis tool and should not be disabled without understanding their purpose. + +## Critical Security Rules (FORBIDDEN) + +### `panic = "forbid"` + +**Intent**: Panics crash the binary analysis tool, leaving users without results. In a CLI tool context, this is unacceptable. +**Why not to disable**: A crashed tool provides no value to users analyzing binaries. + +### `unwrap_used = "forbid"` + +**Intent**: Unwraps can cause unexpected crashes. Production CLI tools must handle all error cases gracefully. +**Why not to disable**: Silent failures in error handling can mask parsing errors or provide incomplete results. + +### `await_holding_lock = "deny"` + +**Intent**: Prevents deadlocks in async code (though Stringy is synchronous, this rule still applies if async is added). +**Why not to disable**: Deadlocks freeze the tool, providing no value to users. + +## Memory Safety Rules + +### `as_conversions = "warn"` + +**Intent**: Prevents potentially lossy type conversions that could corrupt data or cause security bypasses. +**Why not to disable**: Data corruption in security monitoring can lead to false negatives. + +### `as_ptr_cast_mut = "warn"` + +**Intent**: Prevents dangerous mutable pointer casts that could lead to memory corruption or use-after-free. +**Why not to disable**: Memory corruption in security-critical code is a major vulnerability. + +### `cast_ptr_alignment = "warn"` + +**Intent**: Ensures pointer alignment is correct to prevent undefined behavior and potential crashes. +**Why not to disable**: Misaligned pointers can cause crashes or security issues. + +### `indexing_slicing = "warn"` + +**Intent**: Prevents out-of-bounds array access that could cause crashes when parsing binary data. +**Why not to disable**: Buffer overflows when parsing binary sections can crash the tool or produce incorrect results. + +## Arithmetic Safety Rules + +### `arithmetic_side_effects = "warn"` + +**Intent**: Catches unintended arithmetic operations that could lead to incorrect calculations or security bypasses. +**Why not to disable**: Incorrect arithmetic in security calculations can create vulnerabilities. + +### `integer_division = "warn"` + +**Intent**: Warns about potential division by zero that could crash the system. +**Why not to disable**: Division by zero crashes can be exploited or cause monitoring failures. + +### `modulo_arithmetic = "warn"` + +**Intent**: Prevents modulo by zero errors that could crash the system. +**Why not to disable**: Similar to division by zero, this can cause system crashes. + +### `float_cmp = "warn"` + +**Intent**: Ensures safe floating-point comparisons to prevent incorrect security decisions. +**Why not to disable**: Incorrect float comparisons can lead to wrong threat assessments. + +## Performance Rules + +### `clone_on_ref_ptr = "warn"` + +**Intent**: Prevents unnecessary cloning of reference-counted types that wastes memory and CPU. +**Why not to disable**: In a monitoring system, performance directly impacts detection capability. + +### `rc_buffer = "warn"` + +**Intent**: Optimizes reference-counted buffer usage for better performance. +**Why not to disable**: Poor buffer management can cause memory pressure and slow detection. + +### `rc_mutex = "warn"` + +**Intent**: Warns about inefficient reference-counted mutex usage that can cause contention. +**Why not to disable**: Lock contention can slow down threat detection. + +### `large_stack_arrays = "warn"` + +**Intent**: Prevents stack overflow from large arrays that could crash the system. +**Why not to disable**: Stack overflows can crash the monitoring system. + +### `str_to_string = "warn"` + +**Intent**: Avoids unnecessary string allocations that waste memory. +**Why not to disable**: String allocation overhead can impact performance in high-throughput monitoring. + +### `string_add = "warn"` + +**Intent**: Prevents inefficient string concatenation that can cause performance issues. +**Why not to disable**: String concatenation performance matters in log processing. + +### `string_add_assign = "warn"` + +**Intent**: Optimizes string building operations for better performance. +**Why not to disable**: String building is common in alert generation and logging. + +### `unused_async = "warn"` + +**Intent**: Removes unnecessary async overhead that wastes resources. +**Why not to disable**: Unnecessary async can impact system responsiveness. + +## Correctness Rules + +### `correctness = { level = "deny", priority = -1 }` + +**Intent**: Denies all correctness issues that could lead to bugs or security vulnerabilities. +**Why not to disable**: Correctness is fundamental to security monitoring. + +### `suspicious = { level = "warn", priority = -1 }` + +**Intent**: Warns about suspicious patterns that might indicate bugs or security issues. +**Why not to disable**: Suspicious patterns often indicate real problems. + +### `perf = { level = "warn", priority = -1 }` + +**Intent**: Optimizes performance-critical code paths. +**Why not to disable**: Performance directly impacts security monitoring effectiveness. + +## Error Handling Rules + +### `expect_used = "warn"` + +**Intent**: Prefers proper error handling over expect() for better error messages and handling. +**Why not to disable**: Proper error handling is crucial for debugging security issues. + +### `map_err_ignore = "warn"` + +**Intent**: Ensures error transformations are meaningful and not ignored. +**Why not to disable**: Ignored errors can mask security problems. + +### `let_underscore_must_use = "warn"` + +**Intent**: Prevents ignoring important return values that might indicate errors. +**Why not to disable**: Ignored return values can hide security-relevant information. + +## Code Organization Rules + +### `missing_docs_in_private_items = "allow"` + +**Intent**: Private items don't need documentation to reduce noise. +**Why this exception**: Private implementation details don't need public documentation. + +### `redundant_type_annotations = "warn"` + +**Intent**: Removes unnecessary type annotations that clutter code. +**Why not to disable**: Clean code is easier to audit for security issues. + +### `ref_binding_to_reference = "warn"` + +**Intent**: Prevents unnecessary reference binding that can hide ownership issues. +**Why not to disable**: Ownership issues can lead to use-after-free vulnerabilities. + +### `pattern_type_mismatch = "warn"` + +**Intent**: Ensures pattern matching is type-safe to prevent runtime errors. +**Why not to disable**: Type mismatches can cause crashes or security bypasses. + +## Additional Security Rules + +### `dbg_macro = "warn"` + +**Intent**: Prevents debug output from accidentally reaching production logs. +**Why not to disable**: Debug output in production can leak sensitive information. + +### `todo = "warn"` + +**Intent**: Ensures TODO comments are addressed before production deployment. +**Why not to disable**: Unfinished code in production monitoring is a security risk. + +### `unimplemented = "warn"` + +**Intent**: Prevents unimplemented code from reaching production. +**Why not to disable**: Unimplemented code will panic at runtime. + +### `unreachable = "warn"` + +**Intent**: Identifies unreachable code that might indicate logic errors. +**Why not to disable**: Unreachable code often indicates security bypasses or bugs. + +## Performance and Resource Rules + +### `create_dir = "warn"` + +**Intent**: Ensures directory creation is handled properly to prevent race conditions. +**Why not to disable**: Race conditions in file operations can cause security issues. + +### `exit = "warn"` + +**Intent**: Prevents unexpected program termination that could leave systems unmonitored. +**Why not to disable**: Unexpected exits can leave security gaps. + +### `filetype_is_file = "warn"` + +**Intent**: Ensures proper file type checking to prevent security bypasses. +**Why not to disable**: Incorrect file type checks can lead to security vulnerabilities. + +### `float_equality_without_abs = "warn"` + +**Intent**: Prevents incorrect floating-point comparisons that could affect security calculations. +**Why not to disable**: Incorrect comparisons can lead to wrong security decisions. + +### `if_then_some_else_none = "warn"` + +**Intent**: Identifies potentially confusing conditional logic that might hide bugs. +**Why not to disable**: Confusing logic can hide security vulnerabilities. + +### `lossy_float_literal = "warn"` + +**Intent**: Prevents precision loss in floating-point calculations that could affect security metrics. +**Why not to disable**: Precision loss can lead to incorrect security assessments. + +### `match_same_arms = "warn"` + +**Intent**: Identifies duplicate match arms that might indicate copy-paste errors or logic bugs. +**Why not to disable**: Duplicate arms can hide security logic errors. + +### `missing_assert_message = "warn"` + +**Intent**: Ensures assertions have meaningful messages for debugging security issues. +**Why not to disable**: Good assertion messages are crucial for security debugging. + +### `mixed_read_write_in_expression = "warn"` + +**Intent**: Prevents confusing read/write operations that could hide race conditions. +**Why not to disable**: Race conditions can lead to security vulnerabilities. + +### `mutex_atomic = "warn"` + +**Intent**: Suggests using atomic operations instead of mutexes for better performance. +**Why not to disable**: Performance matters in high-throughput security monitoring. + +### `mutex_integer = "warn"` + +**Intent**: Suggests using atomic integers instead of mutex-protected integers. +**Why not to disable**: Atomic operations are more efficient for simple data. + +### `non_ascii_literal = "warn"` + +**Intent**: Ensures non-ASCII literals are intentional and properly handled. +**Why not to disable**: Improper handling of non-ASCII can lead to security bypasses. + +### `non_send_fields_in_send_ty = "warn"` + +**Intent**: Ensures thread safety in async code that processes security data. +**Why not to disable**: Thread safety is crucial for concurrent security processing. + +### `partial_pub_fields = "warn"` + +**Intent**: Prevents partially public structs that can break encapsulation. +**Why not to disable**: Encapsulation is important for security-critical data structures. + +### `same_name_method = "warn"` + +**Intent**: Prevents method name conflicts that could lead to confusion or bugs. +**Why not to disable**: Confusing method names can hide security logic errors. + +### `self_named_module_files = "warn"` + +**Intent**: Ensures consistent module naming that aids in code organization and security auditing. +**Why not to disable**: Consistent naming helps with security code reviews. + +### `semicolon_inside_block = "warn"` + +**Intent**: Prevents confusing semicolon usage that could change code behavior. +**Why not to disable**: Incorrect semicolons can change security logic. + +### `semicolon_outside_block = "warn"` + +**Intent**: Ensures proper semicolon usage for clear code structure. +**Why not to disable**: Clear code structure aids in security auditing. + +### `shadow_reuse = "warn"` + +**Intent**: Prevents variable shadowing that can hide bugs or security issues. +**Why not to disable**: Variable shadowing can hide security logic errors. + +### `shadow_same = "warn"` + +**Intent**: Prevents shadowing variables with the same name. +**Why not to disable**: Same-name shadowing can hide bugs. + +### `shadow_unrelated = "warn"` + +**Intent**: Prevents shadowing unrelated variables that can cause confusion. +**Why not to disable**: Unrelated shadowing can hide security bugs. + +### `string_lit_as_bytes = "warn"` + +**Intent**: Prevents unnecessary string literal to bytes conversion. +**Why not to disable**: Unnecessary conversions waste resources in high-throughput monitoring. + +### `string_slice = "warn"` + +**Intent**: Optimizes string slicing operations for better performance. +**Why not to disable**: String operations are common in log processing and alert generation. + +### `suspicious_operation_groupings = "warn"` + +**Intent**: Identifies suspicious operation groupings that might indicate bugs. +**Why not to disable**: Suspicious patterns often indicate real security issues. + +### `trailing_empty_array = "warn"` + +**Intent**: Prevents trailing empty arrays that can cause confusion or bugs. +**Why not to disable**: Confusing array structures can hide security bugs. + +### `transmute_undefined_repr = "warn"` + +**Intent**: Prevents undefined behavior from transmute operations. +**Why not to disable**: Undefined behavior can lead to security vulnerabilities. + +### `trivial_regex = "warn"` + +**Intent**: Identifies trivial regex patterns that could be simplified. +**Why not to disable**: Simple patterns are easier to audit for security issues. + +### `undocumented_unsafe_blocks = "warn"` + +**Intent**: Ensures unsafe blocks are documented to explain their necessity. +**Why not to disable**: Unsafe code must be carefully documented for security auditing. + +### `unnecessary_self_imports = "warn"` + +**Intent**: Removes unnecessary self imports that clutter code. +**Why not to disable**: Clean code is easier to audit for security issues. + +### `unseparated_literal_suffix = "warn"` + +**Intent**: Ensures proper literal suffix formatting for readability. +**Why not to disable**: Readable code aids in security auditing. + +### `unused_peekable = "warn"` + +**Intent**: Removes unused peekable iterators that waste resources. +**Why not to disable**: Unused resources can impact performance in monitoring systems. + +### `unused_rounding = "warn"` + +**Intent**: Removes unused rounding operations that waste CPU cycles. +**Why not to disable**: CPU cycles matter in high-throughput security monitoring. + +### `use_debug = "warn"` + +**Intent**: Prevents debug formatting in production code that can leak information. +**Why not to disable**: Debug formatting can leak sensitive information in logs. + +### `verbose_file_reads = "warn"` + +**Intent**: Optimizes file reading operations for better performance. +**Why not to disable**: File I/O performance matters in log processing. + +### `wildcard_enum_match_arm = "warn"` + +**Intent**: Prevents wildcard enum matching that can hide security logic errors. +**Why not to disable**: Wildcard matching can hide important security cases. + +### `zero_sized_map_values = "warn"` + +**Intent**: Identifies zero-sized map values that might indicate inefficient data structures. +**Why not to disable**: Inefficient data structures can impact monitoring performance. + +## Pragmatic Exceptions + +These exceptions are allowed currently while the project is in early development. + +### `missing_errors_doc = "allow"` + +**Intent**: Error documentation can be verbose and obvious from context. +**Why this exception**: Reduces noise while maintaining code clarity. + +### `missing_panics_doc = "allow"` + +**Intent**: Panic documentation is often obvious from the panic message. +**Why this exception**: Reduces documentation overhead for obvious cases. + +### `must_use_candidate = "allow"` + +**Intent**: Some must-use candidates are too noisy for this project. +**Why this exception**: Balances safety with developer productivity. + +### `cast_possible_truncation = "allow"` + +**Intent**: Some truncation warnings are too noisy for this project. +**Why this exception**: Reduces noise while maintaining type safety. + +### `cast_precision_loss = "allow"` + +**Intent**: Some precision loss warnings are acceptable in this context. +**Why this exception**: Balances precision with practical considerations. + +### `cast_sign_loss = "allow"` + +**Intent**: Some sign loss warnings are acceptable in this context. +**Why this exception**: Reduces noise while maintaining correctness. + +### `module_name_repetitions = "allow"` + +**Intent**: Module name repetitions are sometimes necessary for clarity. +**Why this exception**: Allows clear module organization. + +### `similar_names = "allow"` + +**Intent**: Similar names are sometimes necessary for related functionality. +**Why this exception**: Reduces noise while maintaining clear naming. + +### `too_many_lines = "allow"` + +**Intent**: Some modules are naturally large due to their complexity. +**Why this exception**: Allows complex modules when necessary. + +### `type_complexity = "allow"` + +**Intent**: Complex types are sometimes necessary for security-critical code. +**Why this exception**: Balances complexity with functionality. + +### `async_yields_async = "allow"` + +**Intent**: Some async yields are necessary for proper async patterns. +**Why this exception**: Allows necessary async patterns. + +### `large_futures = "allow"` + +**Intent**: Some futures are naturally large due to their complexity. +**Why this exception**: Allows complex futures when necessary. + +### `result_large_err = "allow"` + +**Intent**: Some error types are naturally large due to their complexity. +**Why this exception**: Allows complex error types when necessary. + +### `cargo_common_metadata = "allow"` + +**Intent**: Common metadata warnings are too noisy for this project. +**Why this exception**: Reduces noise while maintaining package metadata. + +## Summary + +These linting rules are carefully chosen for a binary analysis tool. Each rule serves a specific purpose in preventing crashes, ensuring performance, and maintaining code quality. They should not be disabled without understanding their intent and the implications of doing so. + +## AI Assistant Restrictions + +**CRITICAL**: AI assistants are explicitly prohibited from removing clippy restrictions or allowing linters marked as `deny` without explicit permission. All `-D warnings` and `deny` attributes must be preserved. Any changes to linting configuration require explicit user approval. diff --git a/.cursor/rules/rust/performance-optimization.mdc b/.cursor/rules/rust/performance-optimization.mdc new file mode 100644 index 0000000..1cdddfc --- /dev/null +++ b/.cursor/rules/rust/performance-optimization.mdc @@ -0,0 +1,275 @@ +--- +globs: **/benches/**/*.rs,**/*bench*.rs,**/*performance*.rs +alwaysApply: false +--- + +# Performance Optimization Standards for Stringy + +## High-Performance Binary Processing + +Stringy uses idiomatic best practices for high-performance binary analysis: + +- **Zero-Copy Parsing**: Use `goblin` for efficient binary format parsing without unnecessary allocations +- **Memory-Mapped Files**: Consider memory-mapped I/O for large binary files +- **Lazy Evaluation**: Process sections on-demand rather than loading everything into memory +- **Efficient String Extraction**: Use slice-based operations for string extraction to avoid allocations + +## Performance Targets + +Stringy must meet strict performance requirements: + +- **Large Binary Processing**: Handle binaries up to several GB efficiently +- **Memory Usage**: Minimize memory footprint, especially for large binaries +- **Processing Speed**: Process typical binaries (10-100 MB) in < 1 second +- **String Extraction**: Extract strings from sections efficiently without excessive allocations +- **Format Detection**: Detect binary format in < 10ms + +## Benchmarking with Criterion + +Use Criterion for performance benchmarking: + +```rust +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::fs; + +fn benchmark_format_detection(c: &mut Criterion) { + let mut group = c.benchmark_group("format_detection"); + + let elf_data = fs::read("test_data/sample.elf").unwrap(); + let pe_data = fs::read("test_data/sample.exe").unwrap(); + let macho_data = fs::read("test_data/sample.macho").unwrap(); + + group.bench_function("detect_elf", |b| { + b.iter(|| black_box(stringy::container::detect_format(&elf_data))) + }); + + group.bench_function("detect_pe", |b| { + b.iter(|| black_box(stringy::container::detect_format(&pe_data))) + }); + + group.bench_function("detect_macho", |b| { + b.iter(|| black_box(stringy::container::detect_format(&macho_data))) + }); + + group.finish(); +} + +fn benchmark_string_extraction(c: &mut Criterion) { + let mut group = c.benchmark_group("string_extraction"); + + let binary_data = fs::read("test_data/large_binary").unwrap(); + let container_info = stringy::container::parse_binary(&binary_data).unwrap(); + + group.bench_function("extract_strings", |b| { + b.iter(|| { + black_box(stringy::extraction::extract_strings( + &binary_data, + &container_info.sections + )) + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + benchmark_format_detection, + benchmark_string_extraction +); +criterion_main!(benches); +``` + +## Memory Management + +Implement efficient memory usage patterns for binary processing: + +```rust +use std::fs::File; +use memmap2::MmapOptions; + +// Memory-mapped file for large binaries +fn process_large_binary(path: &Path) -> Result { + let file = File::open(path)?; + let mmap = unsafe { MmapOptions::new().map(&file)? }; + + // Process memory-mapped data without loading entire file + let format = detect_format(&mmap); + let parser = create_parser(format)?; + let container_info = parser.parse(&mmap)?; + + Ok(container_info) +} + +// Slice-based string extraction to avoid allocations +fn extract_strings_efficient(data: &[u8]) -> Vec { + let mut strings = Vec::new(); + let mut i = 0; + + while i < data.len() { + if let Some(string) = find_string_at_offset(data, i) { + strings.push(string); + i += string.length as usize; + } else { + i += 1; + } + } + + strings +} +``` + +## Binary Parsing Optimization + +Optimize binary format parsing: + +```rust +// Use goblin's zero-copy parsing +fn parse_elf_efficient(data: &[u8]) -> Result { + let elf = goblin::elf::Elf::parse(data)?; + + // Process sections without cloning + let sections: Vec = elf.section_headers + .iter() + .enumerate() + .filter_map(|(idx, header)| { + // Only process sections that are likely to contain strings + if is_string_section(&elf, idx) { + Some(parse_section_info(&elf, header, idx)) + } else { + None + } + }) + .collect(); + + Ok(ContainerInfo { + format: BinaryFormat::Elf, + sections, + imports: extract_imports(&elf)?, + exports: extract_exports(&elf)?, + }) +} +``` + +## String Extraction Optimization + +Optimize string extraction algorithms: + +```rust +// Efficient UTF-8 string extraction +fn extract_utf8_strings(data: &[u8], min_len: usize) -> Vec { + let mut strings = Vec::new(); + let mut start = None; + + for (i, &byte) in data.iter().enumerate() { + if byte.is_ascii() && byte >= 0x20 && byte < 0x7F { + if start.is_none() { + start = Some(i); + } + } else if byte == 0 { + if let Some(s) = start { + let len = i - s; + if len >= min_len { + if let Ok(text) = std::str::from_utf8(&data[s..i]) { + strings.push(FoundString { + text: text.to_string(), + offset: s as u64, + length: len as u32, + // ... other fields + }); + } + } + } + start = None; + } else { + start = None; + } + } + + strings +} +``` + +## Section Processing Optimization + +Process sections efficiently: + +```rust +// Process sections in priority order (highest weight first) +fn process_sections_prioritized( + data: &[u8], + sections: &[SectionInfo] +) -> Vec { + let mut sections = sections.to_vec(); + + // Sort by weight (descending) to process high-value sections first + sections.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap()); + + let mut all_strings = Vec::new(); + + for section in sections { + if let Ok(strings) = extract_strings_from_section(data, §ion) { + all_strings.extend(strings); + } + } + + all_strings +} +``` + +## Performance Testing + +Include performance regression tests: + +```rust +#[test] +fn test_format_detection_performance() { + let data = include_bytes!("../test_data/sample.elf"); + let start = Instant::now(); + + for _ in 0..1000 { + let _format = detect_format(data); + } + + let duration = start.elapsed(); + + // Must complete 1000 detections in < 100ms + assert!(duration < Duration::from_millis(100)); +} + +#[test] +fn test_large_binary_processing() { + let data = fs::read("test_data/large_binary").unwrap(); + let start = Instant::now(); + + let format = detect_format(&data); + let parser = create_parser(format).unwrap(); + let _container_info = parser.parse(&data).unwrap(); + + let duration = start.elapsed(); + + // Must process 100MB binary in < 2 seconds + assert!(duration < Duration::from_secs(2)); +} +``` + +## Memory Usage Testing + +Test memory efficiency: + +```rust +#[test] +fn test_memory_efficiency() { + let data = fs::read("test_data/large_binary").unwrap(); + + // Process binary multiple times + for _ in 0..10 { + let format = detect_format(&data); + let parser = create_parser(format).unwrap(); + let _container_info = parser.parse(&data).unwrap(); + } + + // Memory should not grow unbounded + // (In a real test, you'd measure actual memory usage) +} +``` diff --git a/.cursor/rules/rust/rust-standards.mdc b/.cursor/rules/rust/rust-standards.mdc new file mode 100644 index 0000000..f17a407 --- /dev/null +++ b/.cursor/rules/rust/rust-standards.mdc @@ -0,0 +1,42 @@ +--- +globs: **/*.rs +alwaysApply: false +--- +# Rust Coding Standards for Stringy + +## Language and Edition + +- Always use **Rust 2024 Edition** (MSRV: 1.91+) as specified in [Cargo.toml](mdc:Cargo.toml) +- Follow the package configuration in [Cargo.toml](mdc:Cargo.toml) with `unsafe_code = "forbid"` and `warnings = "deny"` + +## Code Quality Requirements + +- **Zero warnings policy**: All code must pass `cargo clippy -- -D warnings` +- **No unsafe code**: `unsafe_code = "forbid"` is enforced at package level +- **Formatting**: Use standard `rustfmt` with project-specific line length +- **Error Handling**: Use `thiserror` for structured errors +- **Synchronous Design**: This is a synchronous CLI tool - no async runtime needed +- **Focused and Manageable Files**: Source files should be focused and manageable. Large files should be split into smaller, more focused files; no larger than 500-600 lines, when possible. +- **Strictness**: `warnings = "deny"` enforced at package level; any use of `allow` **MUST** be accompanied by a justification in the code and cannot be applied to entire files or modules. + +## Code Organization + +- Use trait-based interfaces for format parsers (`ContainerParser` trait) +- Implement comprehensive error handling with `thiserror` +- Use strongly-typed structures with `serde` for serialization +- Organize by domain: `container/`, `extraction/`, `classification/`, `output/`, `types/` + +## Module Structure + +- **container/**: Binary format detection and parsing (ELF, PE, Mach-O) +- **extraction/**: String extraction algorithms +- **classification/**: Semantic analysis and tagging +- **output/**: Result formatting (JSON, human-readable, YARA-friendly) +- **types/**: Core data structures and error handling + +## Testing Requirements + +- Include comprehensive tests with `insta` for snapshot testing +- Test binary format detection and parsing +- Test string extraction from various formats +- Use `tempfile` for temporary binary files in tests From fffa14b1183e28af583195027b28a9a14da6bfdd Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 21:10:57 -0500 Subject: [PATCH 08/13] Add new command documentation for CI checks, CodeRabbit reviews, code reviews, performance tuning, security hardening, LLM updates, and task management - Introduced comprehensive documentation for CI checks to ensure code changes pass all checks before merging. - Added guidelines for using CodeRabbit to identify and address code issues. - Created a detailed code review process focusing on quality improvements while preserving public APIs. - Documented performance tuning strategies for analyzing and optimizing code performance. - Established security hardening practices to enhance the security posture of the codebase. - Updated the llms.txt file to reflect changes in documentation and specifications. - Added a workflow for managing the next task in the checklist, ensuring compliance with project standards. Signed-off-by: UncleSp1d3r --- .cursor/commands/ci_check.md | 30 ++ .cursor/commands/code_rabbit.md | 28 ++ .cursor/commands/code_review.md | 62 ++++ .cursor/commands/performance_tuning.md | 83 ++++++ .cursor/commands/security_hardening.md | 82 ++++++ .cursor/commands/update_llmstxt.md | 388 +++++++++++++++++++++++++ .cursor/commands/work_next_task.md | 42 +++ 7 files changed, 715 insertions(+) create mode 100644 .cursor/commands/ci_check.md create mode 100644 .cursor/commands/code_rabbit.md create mode 100644 .cursor/commands/code_review.md create mode 100644 .cursor/commands/performance_tuning.md create mode 100644 .cursor/commands/security_hardening.md create mode 100644 .cursor/commands/update_llmstxt.md create mode 100644 .cursor/commands/work_next_task.md diff --git a/.cursor/commands/ci_check.md b/.cursor/commands/ci_check.md new file mode 100644 index 0000000..83138a0 --- /dev/null +++ b/.cursor/commands/ci_check.md @@ -0,0 +1,30 @@ +# CI Check + +## Description + +Ensure code changes pass all CI checks before merging. + +## Steps + +1. First, run `just ci-check` to identify any failures +2. Analyze the output to understand what specific checks are failing. If everything passes, you’re done. +3. Make minimal, targeted fixes to address ONLY the failing checks: + - For formatting issues: run `just format` + - For linting issues (clippy): fix the specific violations reported (rerun with `just lint-rust` / `just lint-rust-min`) + - For compilation/type errors: fix the underlying Rust code until `just check` (or `cargo check`) succeeds + - For test failures: fix the failing tests or underlying code (verify with `just test` or `just test-ci`) + - For dependency security/advisory issues: run `just audit` (cargo-audit) and/or update `Cargo.toml` then `cargo update` + - For license/compliance issues: run `just deny` and address cargo-deny findings +4. After making fixes, run `just ci-check` again to verify all checks pass +5. If any checks still fail, repeat steps 2-4 until all checks pass +6. Provide a summary of what was fixed and confirm that `just ci-check` now passes completely + +Keep changes minimal and focused - only fix what's actually causing the CI failures. Do not make unnecessary refactoring or style changes beyond what's required to pass the checks. + +## Completion Checklist + +- [ ] Code conforms to Stringy project rules and standards +- [ ] Tests pass (`just test`) +- [ ] Linting is clean (`just lint`) +- [ ] Full CI validation passes (`just ci-check`) +- [ ] A short summary of what was done is reported diff --git a/.cursor/commands/code_rabbit.md b/.cursor/commands/code_rabbit.md new file mode 100644 index 0000000..5641f5e --- /dev/null +++ b/.cursor/commands/code_rabbit.md @@ -0,0 +1,28 @@ +# CodeRabbit Review + +## Description + +Use CodeRabbit to identify issues and follow its recommendations in the current code branch. + +## Steps + +1. Run `coderabbit --prompt-only`, let it take as long as it needs to identify issues with this code branch. It will output a large list of recommended fixes and considerations. +2. Evaluate the fixes and considerations. Fix major issues only, or fix any critical issues and ignore the nits. +3. Once those changes are implemented, run CodeRabbit CLI one more time to make sure we addressed all the critical issues and didn't introduce any additional bugs. +4. Do not change branches or mess with `git` at all. Just run the coderabbit tool, examine its output, fix its findings, and run it again to make sure you fixed everything. +5. Then run `just ci-check` to make sure you didn't break anything and, if it does not complete without failures, fix those problems. +6. Only run the loop (running coderabbit->fixing its recommendations->running `just ci-check`->fixing any failures) twice. +7. If on the second run you don't find any critical issues, ignore the nits and you're complete. +8. Give me a summary of everything that was completed and why. + +## Completion Checklist + +- [ ] Code conforms to Stringy project rules and standards +- [ ] Tests pass (`just test`) +- [ ] Linting is clean (`just lint`) +- [ ] Full CI validation passes (`just ci-check`) +- [ ] A short summary of what was done is reported +- [ ] CodeRabbit issues have been addressed +- [ ] CodeRabbit was run no more than twice +- [ ] No unnecessary changes were made beyond addressing critical issues +- [ ] No changes to git branches or history were made diff --git a/.cursor/commands/code_review.md b/.cursor/commands/code_review.md new file mode 100644 index 0000000..e17df87 --- /dev/null +++ b/.cursor/commands/code_review.md @@ -0,0 +1,62 @@ +# Code Review + +## Description + +Analyze diff for code quality issues and apply safe improvements while preserving public APIs. + +## Focus Categories + +Analyze only the changed files (diff scope) and improve them while preserving public APIs. Focus categories: (1) Code Smells (large/duplicate/complex) (2) Design Patterns (traits, builder, newtype, factory) (3) Best Practices (Rust 2024, project conventions) (4) Readability (naming, structure, cohesion) (5) Maintainability (modularization, clarity) (6) Performance (binary parsing, memory usage, allocation, zero-copy operations) (7) Type Safety (strong types, avoid needless Option/Result layering) (8) Error Handling (thiserror context, no silent failures). Context: Stringy = zero-warnings, CLI-first, memory conscious, synchronous binary analysis. Prefer clear + correct over clever. + +## Steps + +1. Collect diff file list. 2. Analyze per focus category. 3. Classify each finding: `safe-edit` (apply now), `deferred`, `requires-approval`. 4. Auto-apply only `safe-edit` (mechanical, internal, non-breaking, warning removal, correctness, error handling improvements). 5. Run `just lint` then `just test`. On failure: isolate failing hunk, revert it, re-run, document skip. 6. Generate report (summary table, applied edits + rationale, deferred backlog, approval-needed with risks, next-step roadmap). 7. Output unified diff (never commit). If zero safe edits: state "No safe automatic edits applied" and still output full report. + +## Auto-Edit Constraints (Strict) + +- Scope: Only diff-related files +- Gates: Must pass `just lint` + tests +- User Control: Never commit/stage +- Public API: No signature/visibility/export changes +- Validation: Always run quality gates before reporting + +## Critical Requirements + +- Actionable suggestions (code examples when clearer) +- Auto-apply only clearly safe internal fixes +- Prioritize runtime correctness, safety, type rigor, security posture +- Preserve all public APIs (no signature/visibility changes) +- Avoid cleverness; optimize for clarity & maintainability + +## Repo Rules (Reinforced) + +Zero warnings (clippy -D warnings) | No unsafe | Precise typing | Trait-based parsers | `thiserror` for errors | CLI-first | Memory efficient | Zero-copy parsing where possible | rustdoc for all public APIs + +--- + +## Execution Checklist + +1 Diff scan 2 Analyze 3 Classify 4 Safe edits applied 5 Gates pass 6 Report (summary/applied/deferred/approval-needed/roadmap) 7 Output diff. On blocker: report + remediation guidance. + +## Quick Reference Matrix + +Category -> Examples of Safe Edits: + +- Smells: remove dead code, split oversized internal fn (no visibility change) +- Patterns: introduce small private helper or trait impl internally +- Best Practices: use zero-copy parsing, efficient string extraction +- Readability: rename local vars (non-public), add rustdoc/examples +- Maintainability: extract internal module (keep re-export stable) +- Performance: eliminate needless clone, memoize constant, bound Vec growth, use slice-based operations +- Type Safety: replace `String` boolean flags with small internal enum (private) +- Error Handling: add context via error messages, convert generic String errors to structured variants if already internal + +If ambiguity arises, default to: classify (deferred) instead of applying. + +## Completion Checklist + +- [ ] Code conforms to Stringy project rules and standards +- [ ] Tests pass (`just test`) +- [ ] Linting is clean (`just lint`) +- [ ] Full CI validation passes (`just ci-check`) +- [ ] A short summary of what was done is reported diff --git a/.cursor/commands/performance_tuning.md b/.cursor/commands/performance_tuning.md new file mode 100644 index 0000000..20603e0 --- /dev/null +++ b/.cursor/commands/performance_tuning.md @@ -0,0 +1,83 @@ +# Performance Tuning + +## Description + +Analyze diff for performance, apply safe micro-optimizations, produce report. + +## FOCUS CATEGORIES + +Analyze ONLY changed files (diff scope) for runtime performance characteristics while preserving correctness, public APIs, and security constraints. Apply only clearly safe micro-optimizations. + +01. Algorithmic Complexity (unnecessary O(n^2), repeated scans, avoidable clones) +02. Allocation Behavior (temporary allocations, Vec growth patterns, reserve vs push, string churn) +03. Binary Parsing Efficiency (zero-copy operations, memory-mapped files for large binaries, efficient section iteration) +04. I/O Efficiency (redundant reads, memory-mapped I/O for large files, efficient file handling) +05. Data Structures (better fit: map vs vec scan, small vec, newtype for clarity/perf) +06. Caching & Reuse (recomputing constants, repeated serialization, repeated formatting) +07. Hot Path Error Handling (avoidable string formatting, cheap early exits) +08. String Extraction (efficient UTF-8/UTF-16 parsing, slice-based operations, avoid unnecessary allocations) +09. Memory Footprint (unbounded growth, retain vs shrink_to_fit decisions, large temporary clones, memory-mapped files) +10. Instrumentation (where benchmarks would help future perf investigations) + +## Steps + +1 Diff list → 2 Perf analysis per category → 3 Classify (`safe-edit` / `deferred` / `requires-approval`) → 4 Apply only mechanical, behavior-preserving micro-optimizations (e.g., remove redundant clone, pre-allocate capacity, use zero-copy parsing, optimize string extraction) → 5 Run `just lint` & `just test` → 6 Revert failing hunk if gates fail → 7 Report (summary, applied, deferred, approval-needed, perf notes, next steps) → 8 Output unified diff (no commit). + +If zero safe edits: state "No safe performance edits applied" and still produce full report. + +## SAFE PERFORMANCE EDIT EXAMPLES + +- Replace `clone()` with reference when ownership not required +- Preallocate Vec with `with_capacity` when length is known +- Convert repeated `format!` in loop to pre-built prefix + push_str +- Hoist constant regex / hashers / serializers +- Short-circuit early on empty input slices +- Use iterators instead of temporary Vec collects where semantic match +- Use slice-based string extraction to avoid allocations +- Use memory-mapped files for large binary processing +- Prefer zero-copy parsing with goblin +- Avoid converting to String just to log when `Display` exists + +## AUTO-EDIT CONSTRAINTS (STRICT) + +Scope: diff-only | Gates: `just lint` + tests must pass | No commits | No public signature/visibility changes | Validate after edits | No semantic changes + +## CRITICAL REQUIREMENTS + +- Do not trade readability or security for micro perf +- Never introduce unsafe +- Provide benchmarks only as recommendations (do not add heavy harness automatically) +- Defer structural refactors (module splits) unless trivial & internal +- Avoid premature caching introducing invalidation complexity + +## REPO RULES (REINFORCED) + +Zero warnings | No unsafe | Precise typing | Trait-based parsers | thiserror for errors | CLI-first | Memory efficiency | Zero-copy parsing | rustdoc for public APIs + +## EXECUTION CHECKLIST + +1 Diff scan 2 Analyze perf 3 Classify 4 Apply safe micro-optimizations 5 Gates pass 6 Report 7 Output diff | On blocker: report & remediate guidance. + +## QUICK PERFORMANCE MATRIX + +Category → Sample Safe Edit: + +- Complexity → Replace nested loop with `HashSet` membership check +- Allocation → Pre-size Vec for known iteration length +- Binary Parsing → Use memory-mapped files for large binaries, zero-copy section access +- I/O → Use memory-mapped I/O for large file processing +- Data Structure → Use `SmallVec` for typical \<=8 elements (internal) +- Caching → Hoist constant serialization of static JSON template +- String Extraction → Use slice-based operations, avoid unnecessary String allocations +- Memory Footprint → Replace accumulating Vec with sliding window bound, use memory-mapped files +- Instrumentation → Add benchmark tests for hot path performance + +Ambiguous? Defer and document. + +## Completion Checklist + +- [ ] Code conforms to Stringy project rules and standards +- [ ] Tests pass (`just test`) +- [ ] Linting is clean (`just lint`) +- [ ] Full CI validation passes (`just ci-check`) +- [ ] A short summary of what was done is reported diff --git a/.cursor/commands/security_hardening.md b/.cursor/commands/security_hardening.md new file mode 100644 index 0000000..f87edcc --- /dev/null +++ b/.cursor/commands/security_hardening.md @@ -0,0 +1,82 @@ +# Security Hardening + +## Description + +Analyze diff for security posture, apply safe internal hardening edits, produce report. + +Analyze ONLY changed files (diff scope) for security posture and apply clearly safe hardening improvements while preserving all public APIs. + +## FOCUS CATEGORIES + +01. Memory Safety (no unsafe code, no added unsafe, boundary adherence) +02. Input Validation & Parsing (CLI args, binary format detection, paths) – reject invalid early, no silent defaults +03. Data Handling (no secrets logged, path validation, safe binary parsing, bounds checking) +04. Binary Parsing Safety (validate offsets, check bounds, handle malformed binaries gracefully) +05. Error Handling & Logging Hygiene (no sensitive leakage, structured context, no println! for operational info) +06. Dependency & Surface Minimization (avoid unnecessary crates/features, dead code removal) +07. Defense-in-Depth Opportunities (bounds checking, resource limits, memory usage bounds) +08. Security Regression Risks (stubs flagged, TODOs categorized, unimplemented sections clearly documented) +09. Supply Chain & Build Hygiene (forbid unsafe, clippy -D warnings, deny unknown features) +10. File I/O Safety (validate file paths, handle large files safely, prevent path traversal) + +## Steps + +1 Diff list → 2 Security analysis per category → 3 Classify findings (`safe-edit` / `deferred` / `requires-approval`) → 4 Apply only mechanical non-breaking hardening edits (logging normalization, path validation + bound checks, converting println!/eprintln! to proper error handling, adding `#[deny(unsafe_code)]` locally if missing, adding missing error context, bounds checking for binary parsing) → 5 Run `just lint` & `just test` → 6 Revert any failing hunk → 7 Report (summary, applied, deferred, approval-needed, risk notes, roadmap) → 8 Output unified diff (no commit). + +If zero safe edits: state "No safe security edits applied" and still emit full report. + +## SAFE HARDENING EDIT EXAMPLES + +- Replace `println!/eprintln!` with proper error handling and structured output +- Add bounds checking for binary parsing operations +- Inline guard clauses for obvious panics or unchecked unwraps (if internal) +- Validate file paths and prevent path traversal +- Remove dead code exposing potential attack surface +- Strengthen error messages (no raw system paths if sensitive) +- Add length / size / iteration bounds for unbounded growth structures +- Replace stringly-typed mode flags with private enums +- Ensure all public API doc comments mention security considerations where relevant +- Validate binary format headers before parsing +- Check section offsets and sizes before accessing binary data + +## AUTO-EDIT CONSTRAINTS (STRICT) + +Scope: diff-only | Gates: `just lint` + tests must pass | No commits | No public signature/visibility changes | Validate after edits + +## CRITICAL REQUIREMENTS + +- Preserve functional behavior while reducing risk +- No new dependencies unless strictly necessary for safety +- Avoid speculative rewrites—minimal surface change +- Avoid perf regressions; if added checks are non-trivial mark as deferred +- Do not mask existing errors—surface with context instead + +## REPO RULES (REINFORCED) + +Zero warnings | No unsafe | Precise typing | Trait-based parsers | thiserror for errors | CLI-first | Memory efficiency | Safe binary parsing | Path validation | rustdoc for public APIs + +## EXECUTION CHECKLIST + +1 Diff scan 2 Analyze security 3 Classify 4 Apply safe hardening edits 5 Gates pass 6 Report 7 Output diff | On blocker: report with remediation. + +## QUICK SECURITY MATRIX + +Category → Sample Safe Edit: + +- Memory Safety → Remove unsafe code, add bounds checking +- Input Validation → Add numeric range check before use, validate binary format headers +- Data Handling → Validate file paths, check bounds before binary access +- Binary Parsing → Add offset/size validation, handle malformed binaries gracefully +- Error Handling → Replace raw error chain with safe error messages +- Resource Bounds → Add comment + bound to vector growth pattern, limit memory usage +- Stub Sections → Mark with `SECURITY_TODO:` prefix for tracking + +Ambiguous? Defer and document. + +## Completion Checklist + +- [ ] Code conforms to Stringy project rules and standards +- [ ] Tests pass (`just test`) +- [ ] Linting is clean (`just lint`) +- [ ] Full CI validation passes (`just ci-check`) +- [ ] A short summary of what was done is reported diff --git a/.cursor/commands/update_llmstxt.md b/.cursor/commands/update_llmstxt.md new file mode 100644 index 0000000..0c81cb6 --- /dev/null +++ b/.cursor/commands/update_llmstxt.md @@ -0,0 +1,388 @@ +# Update LLMs.txt File + +## Description + +Update the llms.txt file in the root folder to reflect changes in documentation or specifications following the llms.txt specification at . + +## Steps + +> SCOPE LIMITATION (IMPORTANT – READ FIRST) +> +> When executing this prompt you MUST restrict all repository content analysis exclusively to the already-provided attachments for `#file:../../docs` (including their subpaths such as `docs/src/`), and root-level documentation files. Treat those attachments as complete and authoritative for the purpose of updating `llms.txt`. +> +> DO NOT attempt to read, open, or re-scan any other project files (e.g., `.github/` instructions, source code, lockfiles, coverage reports) even if tools are available. Avoid recursive or repeated attempts to fetch additional instruction files. If a step below would normally "scan the repo", interpret it narrowly: only enumerate Markdown files inside the provided `docs` attachment tree plus top-level root Markdown files that are already known (you may assume `README.md`, `LICENSE`, `SECURITY.md` exist without re-reading them unless their content is required for a description — which it is not). +> +> If a tool invocation would cause broader traversal, SKIP it and proceed using the attachment lists. This prevents infinite loops and unnecessary I/O. The change detection algorithm should operate purely on: +> +> 1. Root-level Markdown files (assumed: `README.md`, `SECURITY.md`, `LICENSE`) +> 2. `docs/src/**/*.md` +> +> Ignore generated HTML in `docs/book/` and any non-Markdown assets. Treat them as excluded artifacts automatically. Do not add them to `llms.txt`. + +Update the existing `llms.txt` file in the root of the repository to reflect changes in documentation, specifications, or repository structure. This file provides high-level guidance to large language models (LLMs) on where to find relevant content for understanding the repository's purpose and specifications. + +--- + +## TL;DR (Quick Start for the Coding Agent) + +Perform these steps in order; do not skip validation: + +1. Read existing `/llms.txt` (if missing, treat as new file creation). +2. Enumerate repo docs: top-level `*.md`, `docs/**`, key `Cargo.toml` metadata, security & contribution files. +3. Detect additions / removals vs current file (simple set diff on relative paths referenced). +4. Classify candidate files using Inclusion Heuristics (see below). +5. Draft updated sections preserving required structure (H1, optional blockquote, H2 category lists). +6. Ensure link syntax `[Readable Name](relative/path.md): concise description`. +7. Run internal validation checklist (structure, dead links, redundancy, ordering, diff sanity). +8. Output ONLY the new `llms.txt` file content (no commentary) when executing the update. + +If any critical invariant fails (see Invariants) you MUST adjust before finalizing. + +--- + +## Primary Directive + +Update the existing `llms.txt` file to maintain accuracy and compliance with the llms.txt specification while reflecting current repository structure and content. The file must remain optimized for LLM consumption while staying human-readable. + +NOTE ON SCOPE ENFORCEMENT: All subsequent references to "scan", "enumerate", or "discover" files are to be interpreted under the Scope Limitation above. Do not widen scope. + +## Analysis and Planning Phase + +Before updating the `llms.txt` file, you must complete a thorough analysis: + +### Step 1: Review Current File and Specification + +- Read the existing `llms.txt` file to understand current structure, if it exists yet +- Review the official specification at to ensure continued compliance +- Identify areas that may need updates based on repository changes + +### Step 2: Repository Structure Analysis + +- Examine the current repository structure using appropriate tools +- Compare current structure with what's documented in existing `llms.txt` +- Identify new directories, files, or documentation that should be included +- Note any removed or relocated files that need to be updated + +### Step 3: Content Discovery and Change Detection + +- Identify new README files and their locations +- Find new documentation files (`.md` files in `/docs/`, `/spec/`, etc.) +- Locate new specification files and their purposes +- Discover new configuration files and their relevance +- Find new example files and code samples +- Identify any changes to existing documentation structure + +### Step 4: Create Update Plan + +Based on your analysis, create a structured plan that includes: + +- Changes needed to maintain accuracy +- New files to be added to the llms.txt +- Outdated references to be removed or updated +- Organizational improvements to maintain clarity + +## Implementation Requirements + +### Format Compliance + +The updated `llms.txt` file must maintain this exact structure per the specification: + +1. **H1 Header**: Single line with repository/project name (required) +2. **Blockquote Summary**: Brief description in blockquote format (optional but recommended) +3. **Additional Details**: Zero or more markdown sections without headings for context +4. **File List Sections**: Zero or more H2 sections containing markdown lists of links + +### Content Requirements + +#### Required Elements + +- **Project Name**: Clear, descriptive title as H1 +- **Summary**: Concise blockquote explaining the repository's purpose +- **Key Files**: Essential files organized by category (H2 sections) + +#### File Link Format + +Each file link must follow: `[descriptive-name](relative-url): optional description` + +#### Section Organization + +Organize files into logical H2 sections such as: + +- **Documentation**: Core documentation files +- **Specifications**: Technical specifications and requirements +- **Examples**: Sample code and usage examples +- **Configuration**: Setup and configuration files +- **Optional**: Secondary files (special meaning - can be skipped for shorter context) + +### Content Guidelines + +#### Language and Style + +- Use concise, clear, unambiguous language +- Avoid jargon without explanation +- Write for both human and LLM readers +- Be specific and informative in descriptions + +#### File Selection Criteria + +Include files that: + +- Explain the repository's purpose and scope +- Provide essential technical documentation +- Show usage examples and patterns +- Define interfaces and specifications +- Contain configuration and setup instructions + +Exclude files that: + +- Are purely implementation details +- Contain redundant information +- Are build artifacts or generated content +- Are not relevant to understanding the project + +--- + +## Inclusion / Exclusion Heuristics + +Score each candidate (keep those scoring >= 2 unless intentionally excluded): + +| Criterion | +1 Signal | +| ---------------------- | -------------------------------------------------- | +| Orientation Value | Explains purpose, architecture, security model | +| Specification | Defines contracts, limits, protocols, data formats | +| Operator Critical | Install, deploy, config, security hardening | +| Cross-Cutting Policy | Contribution, security, licensing, threat model | +| Representative Example | Shows canonical usage or pattern | + +Negative Exclusion Signals (any one usually drops): vendor lock file, autogenerated, code-only without explanatory context, temporary / experimental docs. + +Prefer the smallest representative set when many similar files exist (e.g., keep `architecture.md` but not all derived slide decks or exports). + +--- + +## Invariants (MUST Always Hold) + +01. File name EXACTLY `llms.txt` at repo root. +02. Single leading H1 only (no multiple H1s). +03. All links are relative paths that exist at commit time. +04. No absolute filesystem paths, no external HTTP links inside file list sections (context isolation). +05. No duplicate file references. +06. Descriptions \<= 140 chars, imperative/concise, no trailing periods unless multiple sentences needed. +07. Section order: Documentation → Specifications → Examples → Configuration → Optional (omit empty ones without leaving gaps). +08. Deterministic ordering inside sections (alphabetical by display name unless logical order is strongly beneficial; if logical order used, it must be consistent and minimal). +09. Do not include compiled artifacts, coverage reports, `target/`, lockfiles (unless spec interest), or large binary assets. +10. Preserve semantic meaning: do not rewrite project intent. + +--- + +## Output Contract + +When finalizing, produce ONLY the complete desired contents of `/llms.txt` (no extra markdown fences, no commentary, no diff). If creation is not needed (no changes), you should explicitly state "NO CHANGE" instead of re‑emitting identical content (optimization for agents that may skip writes). + +--- + +## Change Detection Algorithm (Deterministic) + +1. Parse existing file, extract referenced relative paths. +2. Glob for candidate docs: `*.md` in root, `docs/**/*.md`, security & community files (LICENSE, SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT\* if present). +3. Build two sets: CURRENT_REFERENCED, CANDIDATES. +4. NEW = CANDIDATES − CURRENT_REFERENCED filtered through heuristics. +5. STALE = CURRENT_REFERENCED − CANDIDATES (verify truly removed vs renamed via simple name match search). +6. For renames, map old → new path and update entry in place (preserve description with minor adjustments). +7. Recompute categories; if a section becomes empty, drop it. +8. Produce updated ordered lists, ensuring invariants. + +--- + +## Failure Modes & Recovery + +| Failure | Mitigation | +| ------------------------- | ------------------------------------------------------------------------------- | +| Dead link introduced | Re-scan path; if file newly added but uncommitted, note and omit until present. | +| Overly verbose list | Apply heuristics; collapse by linking an umbrella doc instead of every subpage. | +| Missing critical spec | Escalate by adding under Specifications with concise description. | +| Duplicate classification | Keep in first most appropriate section; remove from others. | +| Section bloat (>15 items) | Split logically or prune low-signal entries (score \<2). | + +--- + +## Non-Goals + +- Not a full index of source code. +- Not a changelog replacement. +- Not a substitute for inline docs. +- Avoid summarizing file contents; only describe purpose. + +--- + +## Minimal vs Comprehensive Example (Illustrative) + +Minimal (small repo): 1 H1, 1 blockquote, 1 Documentation section with 3–6 items. Comprehensive (larger repo like this): Up to 5 sections, each ≤ 12 items, Optional section is last and may be omitted in constrained contexts. + +--- + +## Validation Checklist (Condensed) + +Run prior to output: + +1. Structure: Single H1, ordered sections, no empty lists. +2. Links: All referenced relative paths exist (limit scope to root \*.md, docs/src/\*\*/\*.md). +3. No duplicates: A file appears in exactly one section. +4. Descriptions: \<=140 chars, concise, no trailing period unless multi-sentence. +5. Coverage: Include `README.md`, core `docs/src/*.md` (architecture, installation, usage, binary formats). +6. Exclusions: Omit generated `docs/book/**`, binaries, coverage, `target/`, lockfiles. +7. Ordering: Deterministic (alphabetical or intentional logical grouping documented in comments if used). +8. Delta sanity: NEW and STALE sets evaluated under constrained scope. + +--- + +## Execution Steps + +### Step 1: Current State Analysis + +1. Read the existing `llms.txt` file thoroughly +2. Examine the current repository structure completely +3. Compare existing file references with actual repository content +4. Identify outdated, missing, or incorrect references +5. Note any structural issues with the current file + +### Step 2: Content Planning + +1. Determine if the primary purpose statement needs updates +2. Review and update the summary blockquote if needed +3. Plan additions for new files and directories +4. Plan removals for outdated or moved content +5. Reorganize sections if needed for better clarity + +### Step 3: File Updates + +1. Update the existing `llms.txt` file in the repository root +2. Maintain compliance with the exact format specification +3. Add new file references with appropriate descriptions +4. Remove or update outdated references +5. Ensure all links are valid relative paths + +### Step 4: Validation + +1. Verify continued compliance with specification +2. Check that all links are valid and accessible +3. Ensure the file still serves as an effective LLM navigation tool +4. Confirm the file remains both human and machine readable + +## Quality Assurance + +### Format Validation + +- ✅ H1 header with project name +- ✅ Blockquote summary (if included) +- ✅ H2 sections for file lists +- ✅ Proper markdown link format +- ✅ No broken or invalid links +- ✅ Consistent formatting throughout + +### Content Validation + +- ✅ Clear, unambiguous language +- ✅ Comprehensive coverage of essential files +- ✅ Logical organization of content +- ✅ Appropriate file descriptions +- ✅ Serves as effective LLM navigation tool + +### Specification Compliance + +- ✅ Follows format exactly +- ✅ Uses required markdown structure +- ✅ Implements optional sections appropriately +- ✅ File located at repository root (`/llms.txt`) + +## Update Strategy + +### Addition Process + +When adding new content: + +1. Identify the appropriate section for new files +2. Create clear, descriptive names for links +3. Write concise but informative descriptions +4. Maintain alphabetical or logical ordering within sections +5. Consider if new sections are needed for new content types + +### Removal Process + +When removing outdated content: + +1. Verify files are actually removed or relocated +2. Check if relocated files should be updated rather than removed +3. Remove entire sections if they become empty +4. Update cross-references if needed + +### Reorganization Process + +When restructuring content: + +1. Maintain logical flow from general to specific +2. Keep essential documentation in primary sections +3. Move secondary content to "Optional" section if appropriate +4. Ensure new organization improves LLM navigation + +Example structure for `llms.txt`: + +```markdown +# [Repository Name] + +> [Concise description of the repository's purpose and scope] + +[Optional additional context paragraphs without headings] + +## Documentation + +- [Main README](README.md): Primary project documentation and getting started guide +- [Contributing Guide](CONTRIBUTING.md): Guidelines for contributing to the project +- [Code of Conduct](CODE_OF_CONDUCT.md): Community guidelines and expectations + +## Specifications + +- [Technical Specification](spec/technical-spec.md): Detailed technical requirements and constraints +- [API Specification](spec/api-spec.md): Interface definitions and data contracts + +## Examples + +- [Basic Example](examples/basic-usage.md): Simple usage demonstration +- [Advanced Example](examples/advanced-usage.md): Complex implementation patterns + +## Configuration + +- [Setup Guide](docs/setup.md): Installation and configuration instructions +- [Deployment Guide](docs/deployment.md): Production deployment guidelines + +## Optional + +- [Architecture Documentation](docs/architecture.md): Detailed system architecture +- [Design Decisions](docs/decisions.md): Historical design decision records +``` + +Note: The above example block uses illustrative file paths that may not exist in this repository; they are placeholders to demonstrate formatting only. + +## Success Criteria + +The updated `llms.txt` file should: + +1. Accurately reflect the current repository structure and content +2. Maintain compliance with the llms.txt specification +3. Provide clear navigation to essential documentation +4. Remove outdated or incorrect references +5. Include new important files and documentation +6. Maintain logical organization for easy LLM consumption +7. Use clear, unambiguous language throughout +8. Continue to serve both human and machine readers effectively + +--- + +## Agent Implementation Notes + +- Prefer idempotent operations: if no change required, respond with "NO CHANGE". +- If changes small (≤3 edits), still re-emit full file (atomic replace model). +- Use stable naming: Convert file names to Title Case (minus extensions) unless a proper noun/acronym (e.g., "API", "IPC"). +- For Rust workspace crates, generally include only root `README.md` or high-level `lib.rs` doc if it acts as specification (otherwise omit code internals). +- Security-critical docs (SECURITY.md, threat models) ALWAYS included unless empty. +- If both `docs/` and `spec/` contain overlapping material, prefer placing normative protocol details under Specifications, conceptual overviews under Documentation. diff --git a/.cursor/commands/work_next_task.md b/.cursor/commands/work_next_task.md new file mode 100644 index 0000000..e4b30c1 --- /dev/null +++ b/.cursor/commands/work_next_task.md @@ -0,0 +1,42 @@ +# Work Next Task + +## Description + +Work on the next unchecked task in the current task list. + +## Steps + +1. Read the entire currently open task list document before beginning. Do not skip this step. +2. **Gather Context**: Before starting work on the task: + - If the task list is in a folder, check for `requirements.md` and `design.md` files in the same directory and read them for essential context + - If the task item contains a link to a GitHub issue, examine the issue thoroughly for additional context, acceptance criteria, and potential solutions + - Review any referenced documentation or specifications +3. Identify the next unchecked task in the checklist. The task will typically have an associated github issue linked to it with additional context and a potential solution that should be reviewed as well. + +> ⚠️ Important: Some tasks may appear implemented but are still unchecked. You must verify that each task meets all project standards. "Complete" means the code is fully implemented, idiomatic, tested, lint-free, follows Stringy's architecture, and aligns with all coding and architectural rules. + +### Task Execution Process + +- Review the codebase to determine whether the task is already complete **according to project standards**. +- If the task is not fully compliant: + - Make necessary code changes using idiomatic, maintainable approaches following Stringy's patterns. + - Run `just fmt` to apply formatting rules. + - Add or update tests to ensure correctness. + - Run the test suites: + - `just test` + - Fix any failing tests. + - Run the linters: + - `just lint` + - Fix all linter issues. +- Run `just ci-check` to confirm the full codebase passes comprehensive CI validation (format, lint, test, build, audit). + +## Completion Checklist + +- [ ] Code conforms to Stringy project rules and standards +- [ ] Tests pass (`just test`) +- [ ] Linting is clean (`just lint`) +- [ ] Full CI validation passes (`just ci-check`) +- [ ] Task is marked complete in the checklist +- [ ] A short summary of what was done is reported + +> Update the current task list with any items that are implemented and need test coverage, checking off items that have implemented tests. ❌ Do **not** commit or check in any code ⏸️ Do **not** begin another task ✅ Stop and wait for further instruction after completing this task From eede846c960621493303f217285b6ad9144f42de Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 21:16:49 -0500 Subject: [PATCH 09/13] Update Cargo.toml for Rust 2024 Edition and add benchmarking for ELF parsing - Updated the Rust edition in Cargo.toml from 2021 to 2024. - Introduced a new benchmark suite for ELF parsing in `benches/elf.rs`, including tests for full parsing, imports, and exports. - Enhanced documentation in `binary-formats.md` to clarify symbol-to-library mapping and its implementation details. - Modified the ELF parser to support library mapping using version information, improving the accuracy of symbol attribution. Signed-off-by: UncleSp1d3r --- Cargo.toml | 6 +- benches/elf.rs | 72 ++++++ docs/src/binary-formats.md | 66 ++++- src/container/elf.rs | 233 +++++++++++------ tests/integration_elf.rs | 496 +++++++++++++++++++++++++++++-------- 5 files changed, 679 insertions(+), 194 deletions(-) create mode 100644 benches/elf.rs diff --git a/Cargo.toml b/Cargo.toml index 36f62e6..21bf169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stringy" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["UncleSp1d3r "] description = "A smarter alternative to the strings command that leverages format-specific knowledge" license = "Apache-2.0" @@ -34,3 +34,7 @@ tempfile = "3.8" [profile.dist] inherits = "release" lto = "thin" + +[[bench]] +name = "elf" +harness = false diff --git a/benches/elf.rs b/benches/elf.rs new file mode 100644 index 0000000..c98465e --- /dev/null +++ b/benches/elf.rs @@ -0,0 +1,72 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; +use stringy::container::{ContainerParser, ElfParser}; + +fn bench_elf_full_parse(c: &mut Criterion) { + // Use the current test binary as a sample ELF file + let current_exe = std::env::current_exe().expect("Failed to get current executable"); + let data = std::fs::read(¤t_exe).expect("Failed to read test binary"); + + // Only benchmark if it's actually an ELF file + if !stringy::container::ElfParser::detect(&data) { + return; + } + + let parser = ElfParser::new(); + c.bench_function("elf_full_parse", |b| { + b.iter(|| { + let _ = parser.parse(black_box(&data)); + }); + }); +} + +fn bench_elf_parse_with_imports(c: &mut Criterion) { + let current_exe = std::env::current_exe().expect("Failed to get current executable"); + let data = std::fs::read(¤t_exe).expect("Failed to read test binary"); + + if !stringy::container::ElfParser::detect(&data) { + return; + } + + let parser = ElfParser::new(); + c.bench_function("elf_parse_with_imports", |b| { + b.iter(|| { + if let Ok(container_info) = parser.parse(black_box(&data)) { + // Access imports to ensure mapping is performed + let _import_count = container_info.imports.len(); + let _imports_with_libs = container_info + .imports + .iter() + .filter(|imp| imp.library.is_some()) + .count(); + } + }); + }); +} + +fn bench_elf_parse_with_exports(c: &mut Criterion) { + let current_exe = std::env::current_exe().expect("Failed to get current executable"); + let data = std::fs::read(¤t_exe).expect("Failed to read test binary"); + + if !stringy::container::ElfParser::detect(&data) { + return; + } + + let parser = ElfParser::new(); + c.bench_function("elf_parse_with_exports", |b| { + b.iter(|| { + if let Ok(container_info) = parser.parse(black_box(&data)) { + // Access exports to ensure filtering is performed + let _export_count = container_info.exports.len(); + } + }); + }); +} + +criterion_group!( + elf_benches, + bench_elf_full_parse, + bench_elf_parse_with_imports, + bench_elf_parse_with_exports +); +criterion_main!(elf_benches); diff --git a/docs/src/binary-formats.md b/docs/src/binary-formats.md index ad35471..d06cfa5 100644 --- a/docs/src/binary-formats.md +++ b/docs/src/binary-formats.md @@ -34,7 +34,7 @@ The ELF parser now provides comprehensive symbol extraction with: - Supports multiple symbol types: functions, objects, TLS variables, and indirect functions - Handles both global and weak bindings - - Foundation for library mapping through DT_NEEDED analysis + - Maps symbols to their providing libraries using version information 2. **Export Detection**: Extracts all globally visible defined symbols @@ -45,7 +45,14 @@ The ELF parser now provides comprehensive symbol extraction with: 3. **Library Dependencies**: Extracts DT_NEEDED entries from the dynamic section - Provides list of required shared libraries - - Foundation for future symbol-to-library mapping + - Used in conjunction with version information for symbol-to-library mapping + +4. **Symbol-to-Library Mapping**: Maps imported symbols to their providing libraries + + - Uses ELF version tables (versym and verneed) for best-effort attribution + - Process: versym index → verneed entry → library filename + - Falls back to heuristics for unversioned symbols (e.g., common libc symbols) + - Returns `None` when version information is unavailable or ambiguous ### Implementation Details @@ -65,10 +72,11 @@ impl ElfParser { } } - fn extract_imports(&self, elf: &Elf) -> Vec { + fn extract_imports(&self, elf: &Elf, libraries: &[String]) -> Vec { // Extract undefined symbols from dynamic symbol table // Supports STT_FUNC, STT_OBJECT, STT_TLS, STT_GNU_IFUNC, STT_NOTYPE // Handles both STB_GLOBAL and STB_WEAK bindings + // Maps symbols to libraries using version information } fn extract_exports(&self, elf: &Elf) -> Vec { @@ -81,9 +89,61 @@ impl ElfParser { // Parse DT_NEEDED entries from dynamic section // Returns list of required shared library names } + + fn get_symbol_providing_library( + &self, + elf: &Elf, + sym_index: usize, + libraries: &[String], + ) -> Option { + // 1. Get version index from versym table for this symbol + // 2. Look up version in verneed to find library name + // 3. Match with DT_NEEDED entries + // 4. Fallback to heuristics for unversioned symbols + } } ``` +### Library Dependency Mapping + +The ELF parser implements symbol-to-library mapping using ELF version information: + +1. **Version Symbol Table (versym)**: Maps each dynamic symbol to a version index + + - Index 0 (VER_NDX_LOCAL): Local symbol, not available externally + - Index 1 (VER_NDX_GLOBAL): Global symbol, no specific version + - Index ≥ 2: Versioned symbol, references verneed entry + +2. **Version Needed Table (verneed)**: Lists library dependencies with version requirements + + - Each entry contains a library filename (from DT_NEEDED) + - Auxiliary entries specify version names and indices + - Links version indices to specific libraries + +3. **Mapping Process**: + + ``` + Symbol → versym[sym_index] → version_index → verneed lookup → library_name + ``` + +4. **Fallback Strategies**: + + - For unversioned symbols: Attempt to match common symbols (e.g., `printf`, `malloc`) to libc + - If only one library is needed: Attribute to that library (least accurate) + - Otherwise: Return `None` to avoid false positives + +### Limitations + +ELF's indirect linking model means symbol-to-library mapping is best-effort: + +- **Accuracy**: Version-based mapping is accurate when version information is present, but many binaries lack version info +- **Unversioned Symbols**: Symbols without version information cannot be definitively mapped without relocation analysis +- **Relocation Tables**: PLT/GOT relocations would provide definitive mapping but require complex analysis +- **Static Linking**: Statically linked binaries have no dynamic section, so all imports have `library: None` +- **Stripped Binaries**: Stripped binaries may lack symbol tables entirely + +The current implementation is sufficient for most string classification use cases where approximate library attribution is acceptable. + ## PE (Portable Executable) Used on Windows for executables, DLLs, and drivers. diff --git a/src/container/elf.rs b/src/container/elf.rs index 550653b..6d0a7dc 100644 --- a/src/container/elf.rs +++ b/src/container/elf.rs @@ -82,11 +82,12 @@ impl ElfParser { /// Extract import information from ELF dynamic section /// Imports are symbols that are undefined (SHN_UNDEF) and need to be resolved at runtime - fn extract_imports(&self, elf: &Elf) -> Vec { + fn extract_imports(&self, elf: &Elf, libraries: &[String]) -> Vec { let mut imports = Vec::new(); + let mut seen_names = HashSet::new(); // Extract from dynamic symbol table - for sym in &elf.dynsyms { + for (sym_index, sym) in elf.dynsyms.iter().enumerate() { // Import symbols are: // - Undefined (st_shndx == SHN_UNDEF) // - Global or weak binding @@ -99,21 +100,19 @@ impl ElfParser { || sym.st_type() == goblin::elf::sym::STT_TLS || sym.st_type() == goblin::elf::sym::STT_GNU_IFUNC || sym.st_type() == goblin::elf::sym::STT_NOTYPE) + && let Some(name) = elf.dynstrtab.get_at(sym.st_name) + && !name.is_empty() + && seen_names.insert(name.to_string()) { - if let Some(name) = elf.dynstrtab.get_at(sym.st_name) { - // Skip empty names - if !name.is_empty() { - imports.push(ImportInfo { - name: name.to_string(), - library: self.extract_library_from_needed(elf, name), - address: if sym.st_value != 0 { - Some(sym.st_value) - } else { - None - }, - }); - } - } + imports.push(ImportInfo { + name: name.to_string(), + library: self.get_symbol_providing_library(elf, sym_index, libraries), + address: if sym.st_value != 0 { + Some(sym.st_value) + } else { + None + }, + }); } } @@ -127,23 +126,19 @@ impl ElfParser { || sym.st_type() == goblin::elf::sym::STT_TLS || sym.st_type() == goblin::elf::sym::STT_GNU_IFUNC || sym.st_type() == goblin::elf::sym::STT_NOTYPE) + && let Some(name) = elf.strtab.get_at(sym.st_name) + && !name.is_empty() + && seen_names.insert(name.to_string()) { - if let Some(name) = elf.strtab.get_at(sym.st_name) { - if !name.is_empty() { - // Avoid duplicates from dynamic symbol table - if !imports.iter().any(|imp| imp.name == name) { - imports.push(ImportInfo { - name: name.to_string(), - library: None, // Static symbols don't have library info - address: if sym.st_value != 0 { - Some(sym.st_value) - } else { - None - }, - }); - } - } - } + imports.push(ImportInfo { + name: name.to_string(), + library: None, // Static symbols don't have library info + address: if sym.st_value != 0 { + Some(sym.st_value) + } else { + None + }, + }); } } @@ -152,11 +147,9 @@ impl ElfParser { /// Extract DT_NEEDED entries (library dependencies) from ELF dynamic section /// - /// This method is currently used in tests and reserved for future use when implementing - /// symbol-to-library mapping. ELF doesn't directly associate imported symbols with specific - /// libraries without analyzing version symbols or relocation tables, which requires more - /// sophisticated analysis than currently implemented. - #[allow(dead_code)] + /// Returns a list of required shared library names that the binary depends on. + /// These are used in conjunction with version information to map symbols to their + /// providing libraries. fn extract_needed_libraries(&self, elf: &Elf) -> Vec { if let Some(ref dynamic) = elf.dynamic { dynamic @@ -169,18 +162,98 @@ impl ElfParser { } } - /// Attempt to extract library information from DT_NEEDED entries - /// This is a best-effort approach since ELF doesn't directly link symbols to libraries - fn extract_library_from_needed(&self, elf: &Elf, _symbol_name: &str) -> Option { - // For now, we can't reliably determine which specific library a symbol comes from - // in ELF without additional information like version symbols or relocation data. - // This would require more complex analysis of the dynamic linking process. + /// Get the library that provides a symbol using version information + /// This is a best-effort approach using versym and verneed tables + fn get_symbol_providing_library( + &self, + elf: &Elf, + sym_index: usize, + libraries: &[String], + ) -> Option { + // If no libraries are available, return None + if libraries.is_empty() { + return None; + } + + // Try to resolve version information for this symbol + if let Some(version_index) = self.resolve_versym(elf, sym_index) { + // Version index 0 (VER_NDX_LOCAL) and 1 (VER_NDX_GLOBAL) are special + // and don't correspond to specific libraries + if version_index >= 2 + && let Some((library_name, _version_name)) = + self.parse_verneed_entry(elf, version_index) + { + // Match the library name from verneed with DT_NEEDED entries + for lib in libraries { + if lib.contains(&library_name) || library_name.contains(lib) { + return Some(lib.clone()); + } + } + // If exact match not found, return the library name from verneed + return Some(library_name); + } + } + + // Fallback: For common libc symbols, attribute to first libc library found + // This is a heuristic and may not always be accurate + if let Some(libc_lib) = libraries.iter().find(|lib| { + lib.contains("libc") || lib.contains("libSystem") || lib.contains("libc.so") + }) { + return Some(libc_lib.clone()); + } + + // Last resort: return first library (least accurate) + if libraries.len() == 1 { + return Some(libraries[0].clone()); + } + + None + } + + /// Resolve version symbol index from versym table + fn resolve_versym(&self, elf: &Elf, sym_index: usize) -> Option { + // Check if versym table exists and has entry for this symbol + let versym = elf.versym.as_ref()?; + if versym.is_empty() || sym_index >= versym.len() { + return None; + } - // We could potentially return the first DT_NEEDED entry as a fallback, - // but that would be misleading. Better to return None for accuracy. + if let Some(versym_entry) = versym.get_at(sym_index) { + let version_index = versym_entry.vs_val; + // VER_NDX_LOCAL (0) and VER_NDX_GLOBAL (1) are special values + // that don't correspond to versioned symbols + if version_index >= 2 { + return Some(version_index); + } + } + + None + } + + /// Parse verneed entry to extract library name and version name + /// Returns (library_name, version_name) if found + fn parse_verneed_entry(&self, elf: &Elf, version_index: u16) -> Option<(String, String)> { + let verneed = elf.verneed.as_ref()?; + + // Iterate through verneed entries to find the one matching version_index + for verneed_entry in verneed.iter() { + // Extract library name from verneed entry + let library_name = elf + .dynstrtab + .get_at(verneed_entry.vn_file) + .unwrap_or("") + .to_string(); + + // Check auxiliary versions in this verneed entry + for aux in verneed_entry.iter() { + if aux.vna_other == version_index { + // Found matching version, extract version name + let version_name = elf.dynstrtab.get_at(aux.vna_name).unwrap_or("").to_string(); + return Some((library_name, version_name)); + } + } + } - // Future enhancement: analyze PLT/GOT relocations to match symbols to libraries - let _ = elf; // Suppress unused parameter warning None } @@ -202,16 +275,15 @@ impl ElfParser { && sym.st_value != 0 && sym.st_visibility() != goblin::elf::sym::STV_HIDDEN && sym.st_visibility() != goblin::elf::sym::STV_INTERNAL + && let Some(name) = elf.dynstrtab.get_at(sym.st_name) + && !name.is_empty() + && seen_names.insert(name.to_string()) { - if let Some(name) = elf.dynstrtab.get_at(sym.st_name) { - if !name.is_empty() && seen_names.insert(name.to_string()) { - exports.push(ExportInfo { - name: name.to_string(), - address: sym.st_value, - ordinal: None, // ELF doesn't use ordinals - }); - } - } + exports.push(ExportInfo { + name: name.to_string(), + address: sym.st_value, + ordinal: None, // ELF doesn't use ordinals + }); } } @@ -228,16 +300,15 @@ impl ElfParser { || sym.st_type() == goblin::elf::sym::STT_TLS || sym.st_type() == goblin::elf::sym::STT_GNU_IFUNC || sym.st_type() == goblin::elf::sym::STT_NOTYPE) + && let Some(name) = elf.strtab.get_at(sym.st_name) + && !name.is_empty() + && seen_names.insert(name.to_string()) { - if let Some(name) = elf.strtab.get_at(sym.st_name) { - if !name.is_empty() && seen_names.insert(name.to_string()) { - exports.push(ExportInfo { - name: name.to_string(), - address: sym.st_value, - ordinal: None, // ELF doesn't use ordinals - }); - } - } + exports.push(ExportInfo { + name: name.to_string(), + address: sym.st_value, + ordinal: None, // ELF doesn't use ordinals + }); } } @@ -290,7 +361,8 @@ impl ContainerParser for ElfParser { }); } - let imports = self.extract_imports(&elf); + let libraries = self.extract_needed_libraries(&elf); + let imports = self.extract_imports(&elf, &libraries); let exports = self.extract_exports(&elf); Ok(ContainerInfo { @@ -480,7 +552,6 @@ mod tests { // by checking that they compile and can be referenced let _extract_imports = ElfParser::extract_imports; let _extract_exports = ElfParser::extract_exports; - let _extract_library = ElfParser::extract_library_from_needed; // Verify parser can be created (this is a compile-time check) let _ = parser; @@ -495,12 +566,12 @@ mod tests { // We can't use Elf::default() as it doesn't exist, so we'll test the behavior // by verifying that the method signature is correct and the documented behavior - // The extract_library_from_needed method should return None as documented - // since ELF doesn't directly link symbols to libraries without additional analysis + // The get_symbol_providing_library method uses version information to map symbols + // to libraries, which is a best-effort approach - // This is a compile-time test to ensure the method exists with correct signature - let _method_ref: fn(&ElfParser, &Elf, &str) -> Option = - ElfParser::extract_library_from_needed; + // This is a compile-time test to ensure the methods exist with correct signatures + let _method_ref: fn(&ElfParser, &Elf, usize, &[String]) -> Option = + ElfParser::get_symbol_providing_library; // Verify the parser exists let _ = parser; @@ -512,17 +583,17 @@ mod tests { // This test demonstrates the extract_needed_libraries method works with real ELF files let current_exe = std::env::current_exe().expect("Failed to get current executable"); - if let Ok(data) = std::fs::read(¤t_exe) { - if let Ok(goblin::Object::Elf(elf)) = goblin::Object::parse(&data) { - let parser = ElfParser::new(); - let libraries = parser.extract_needed_libraries(&elf); + if let Ok(data) = std::fs::read(¤t_exe) + && let Ok(goblin::Object::Elf(elf)) = goblin::Object::parse(&data) + { + let parser = ElfParser::new(); + let libraries = parser.extract_needed_libraries(&elf); - // The test binary should have some libraries (e.g., libc) unless statically linked - println!("Test binary libraries: {:?}", libraries); + // The test binary should have some libraries (e.g., libc) unless statically linked + println!("Test binary libraries: {:?}", libraries); - // Just verify the method runs without panicking - // Actual library content depends on the build environment - } + // Just verify the method runs without panicking + // Actual library content depends on the build environment } } diff --git a/tests/integration_elf.rs b/tests/integration_elf.rs index 02ed6a5..d0e1c03 100644 --- a/tests/integration_elf.rs +++ b/tests/integration_elf.rs @@ -265,75 +265,71 @@ fn test_elf_section_classification_integration() { // Test with the current binary (this test executable) let current_exe = std::env::current_exe().expect("Failed to get current executable path"); - if let Ok(elf_data) = fs::read(¤t_exe) { - if ElfParser::detect(&elf_data) { - let parser = ElfParser::new(); - if let Ok(container_info) = parser.parse(&elf_data) { - // Verify we found sections and classified them - assert!( - !container_info.sections.is_empty(), - "Should find sections in ELF binary" - ); - - // Look for common ELF sections and verify weights are assigned - let section_names: Vec<&str> = container_info - .sections - .iter() - .map(|sec| sec.name.as_str()) - .collect(); - - println!("Found sections: {:?}", section_names); - - // Verify that all sections have weights assigned - for section in &container_info.sections { - assert!( - section.weight > 0.0, - "Section {} should have a positive weight, got {}", - section.name, - section.weight - ); - } + if let Ok(elf_data) = fs::read(¤t_exe) + && ElfParser::detect(&elf_data) + && let Ok(container_info) = ElfParser::new().parse(&elf_data) + { + // Verify we found sections and classified them + assert!( + !container_info.sections.is_empty(), + "Should find sections in ELF binary" + ); + + // Look for common ELF sections and verify weights are assigned + let section_names: Vec<&str> = container_info + .sections + .iter() + .map(|sec| sec.name.as_str()) + .collect(); + + println!("Found sections: {:?}", section_names); + + // Verify that all sections have weights assigned + for section in &container_info.sections { + assert!( + section.weight > 0.0, + "Section {} should have a positive weight, got {}", + section.name, + section.weight + ); + } - // Check that string data sections get higher weights than code sections - let string_sections: Vec<_> = container_info - .sections - .iter() - .filter(|sec| { - matches!(sec.section_type, stringy::types::SectionType::StringData) - }) - .collect(); - let code_sections: Vec<_> = container_info - .sections - .iter() - .filter(|sec| matches!(sec.section_type, stringy::types::SectionType::Code)) - .collect(); - - if !string_sections.is_empty() && !code_sections.is_empty() { - let max_string_weight = string_sections - .iter() - .map(|s| s.weight) - .fold(0.0f32, f32::max); - let max_code_weight = code_sections - .iter() - .map(|s| s.weight) - .fold(0.0f32, f32::max); - assert!( - max_string_weight > max_code_weight, - "String sections should have higher weight than code sections" - ); - } + // Check that string data sections get higher weights than code sections + let string_sections: Vec<_> = container_info + .sections + .iter() + .filter(|sec| matches!(sec.section_type, stringy::types::SectionType::StringData)) + .collect(); + let code_sections: Vec<_> = container_info + .sections + .iter() + .filter(|sec| matches!(sec.section_type, stringy::types::SectionType::Code)) + .collect(); + + if !string_sections.is_empty() && !code_sections.is_empty() { + let max_string_weight = string_sections + .iter() + .map(|s| s.weight) + .fold(0.0f32, f32::max); + let max_code_weight = code_sections + .iter() + .map(|s| s.weight) + .fold(0.0f32, f32::max); + assert!( + max_string_weight > max_code_weight, + "String sections should have higher weight than code sections" + ); + } - // We should find at least some standard sections - let has_text = section_names.iter().any(|&name| name.contains(".text")); - let has_rodata = section_names.iter().any(|&name| name.contains(".rodata")); + // We should find at least some standard sections + let has_text = section_names.iter().any(|&name| name.contains(".text")); + let has_rodata = section_names.iter().any(|&name| name.contains(".rodata")); - // At least one of these should be present in a typical ELF - assert!( - has_text || has_rodata, - "Should find .text or .rodata sections" - ); - } - } + // At least one of these should be present in a typical ELF + assert!( + has_text || has_rodata, + "Should find .text or .rodata sections" + ); } } @@ -398,60 +394,342 @@ fn test_elf_symbol_extraction_snapshot() { // Test with the current binary to create a snapshot of symbol extraction let current_exe = std::env::current_exe().expect("Failed to get current executable path"); - if let Ok(elf_data) = fs::read(¤t_exe) { - if ElfParser::detect(&elf_data) { - let parser = ElfParser::new(); - if let Ok(container_info) = parser.parse(&elf_data) { - // Create a formatted output for snapshot testing - let mut output = String::new(); - - // Document imports - output.push_str("=== IMPORTS ===\n"); - output.push_str(&format!("Total: {}\n\n", container_info.imports.len())); - - // Take first 10 imports for snapshot (to keep it manageable) - for (i, import) in container_info.imports.iter().take(10).enumerate() { - output.push_str(&format!("Import {}: {}\n", i + 1, import.name)); - if let Some(ref lib) = import.library { - output.push_str(&format!(" Library: {}\n", lib)); + if let Ok(elf_data) = fs::read(¤t_exe) + && ElfParser::detect(&elf_data) + && let Ok(container_info) = ElfParser::new().parse(&elf_data) + { + // Create a formatted output for snapshot testing + let mut output = String::new(); + + // Document imports + output.push_str("=== IMPORTS ===\n"); + output.push_str(&format!("Total: {}\n\n", container_info.imports.len())); + + // Take first 10 imports for snapshot (to keep it manageable) + for (i, import) in container_info.imports.iter().take(10).enumerate() { + output.push_str(&format!("Import {}: {}\n", i + 1, import.name)); + if let Some(ref lib) = import.library { + output.push_str(&format!(" Library: {}\n", lib)); + } + if let Some(addr) = import.address { + output.push_str(&format!(" Address: 0x{:x}\n", addr)); + } + output.push('\n'); + } + + if container_info.imports.len() > 10 { + output.push_str(&format!( + "... and {} more imports\n\n", + container_info.imports.len() - 10 + )); + } + + // Document exports + output.push_str("=== EXPORTS ===\n"); + output.push_str(&format!("Total: {}\n\n", container_info.exports.len())); + + // Take first 10 exports for snapshot + for (i, export) in container_info.exports.iter().take(10).enumerate() { + output.push_str(&format!("Export {}: {}\n", i + 1, export.name)); + output.push_str(&format!(" Address: 0x{:x}\n", export.address)); + if let Some(ord) = export.ordinal { + output.push_str(&format!(" Ordinal: {}\n", ord)); + } + output.push('\n'); + } + + if container_info.exports.len() > 10 { + output.push_str(&format!( + "... and {} more exports\n", + container_info.exports.len() - 10 + )); + } + + // Snapshot the output + assert_snapshot!("elf_symbol_extraction", output); + } +} + +#[test] +#[cfg(target_family = "unix")] +fn test_elf_symbol_library_mapping() { + // Test symbol-to-library mapping using version information + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let c_file = temp_dir.path().join("test_versioned.c"); + let elf_file = temp_dir.path().join("test_versioned"); + + let c_code = r#" + #include + #include + + int main() { + printf("Hello from versioned symbol\n"); // Should map to libc + void* ptr = malloc(100); // Should map to libc + free(ptr); + return 0; + } + "#; + + File::create(&c_file) + .expect("Failed to create C file") + .write_all(c_code.as_bytes()) + .expect("Failed to write C code"); + + // Compile dynamically linked binary (version info typically present) + let mut output = Command::new("x86_64-linux-gnu-gcc") + .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) + .output(); + + if output.is_err() || !output.as_ref().map(|o| o.status.success()).unwrap_or(false) { + output = Command::new("gcc") + .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) + .output(); + } + + match output { + Ok(output) if output.status.success() => { + let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); + + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(_)) => { + let parser = ElfParser::new(); + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); + + // Check that we found imports + assert!(!container_info.imports.is_empty(), "Should find imports"); + + // Check that some imports have library information populated + let imports_with_libs: Vec<_> = container_info + .imports + .iter() + .filter(|imp| imp.library.is_some()) + .collect(); + + println!( + "Found {} imports with library information out of {} total imports", + imports_with_libs.len(), + container_info.imports.len() + ); + + // Common libc symbols should have library info if version info is available + let printf_import = container_info + .imports + .iter() + .find(|imp| imp.name == "printf"); + let malloc_import = container_info + .imports + .iter() + .find(|imp| imp.name == "malloc"); + + if let Some(printf) = printf_import { + println!("printf import: {:?}", printf); + // If version info is available, library should be populated + // Otherwise, it may be None (unversioned or fallback didn't match) } - if let Some(addr) = import.address { - output.push_str(&format!(" Address: 0x{:x}\n", addr)); + + if let Some(malloc) = malloc_import { + println!("malloc import: {:?}", malloc); } - output.push('\n'); - } - if container_info.imports.len() > 10 { - output.push_str(&format!( - "... and {} more imports\n\n", - container_info.imports.len() - 10 - )); + // At least verify the mapping logic runs without errors + // Actual library attribution depends on binary's version info + } + Ok(goblin::Object::Mach(_)) => { + println!("Got Mach-O binary, skipping ELF-specific test"); + } + Ok(_) => { + println!("Got non-ELF binary, skipping test"); + } + Err(e) => { + println!("Failed to parse binary: {}, skipping test", e); } + } + } + Ok(_) => { + println!("Compilation failed, skipping test"); + } + Err(_) => { + println!("GCC not available, skipping test"); + } + } +} + +#[test] +#[cfg(target_family = "unix")] +fn test_elf_unversioned_symbols() { + // Test handling of symbols without version info + let current_exe = std::env::current_exe().expect("Failed to get current executable path"); + + if let Ok(elf_data) = fs::read(¤t_exe) + && ElfParser::detect(&elf_data) + && let Ok(container_info) = ElfParser::new().parse(&elf_data) + { + // Count imports with and without library info + let with_lib = container_info + .imports + .iter() + .filter(|imp| imp.library.is_some()) + .count(); + let without_lib = container_info + .imports + .iter() + .filter(|imp| imp.library.is_none()) + .count(); + + println!( + "Imports with library: {}, without library: {}", + with_lib, without_lib + ); + + // Both cases are valid - versioned symbols get libraries, + // unversioned symbols may not + assert!( + !container_info.imports.is_empty(), + "Should find at least some imports" + ); + } +} + +#[test] +#[cfg(target_family = "unix")] +fn test_elf_no_dynamic_section() { + // Test static binaries (no dynamic section) + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let c_file = temp_dir.path().join("test_static.c"); + let elf_file = temp_dir.path().join("test_static"); + + let c_code = r#" + int main() { + return 0; + } + "#; + + File::create(&c_file) + .expect("Failed to create C file") + .write_all(c_code.as_bytes()) + .expect("Failed to write C code"); - // Document exports - output.push_str("=== EXPORTS ===\n"); - output.push_str(&format!("Total: {}\n\n", container_info.exports.len())); + // Try to compile statically + let mut output = Command::new("x86_64-linux-gnu-gcc") + .args([ + "-static", + "-o", + elf_file.to_str().unwrap(), + c_file.to_str().unwrap(), + ]) + .output(); - // Take first 10 exports for snapshot - for (i, export) in container_info.exports.iter().take(10).enumerate() { - output.push_str(&format!("Export {}: {}\n", i + 1, export.name)); - output.push_str(&format!(" Address: 0x{:x}\n", export.address)); - if let Some(ord) = export.ordinal { - output.push_str(&format!(" Ordinal: {}\n", ord)); + if output.is_err() || !output.as_ref().map(|o| o.status.success()).unwrap_or(false) { + output = Command::new("gcc") + .args([ + "-static", + "-o", + elf_file.to_str().unwrap(), + c_file.to_str().unwrap(), + ]) + .output(); + } + + match output { + Ok(output) if output.status.success() => { + let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); + + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(_)) => { + let parser = ElfParser::new(); + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); + + // Static binaries should have no or very few imports + // and those imports should have library: None + for import in &container_info.imports { + assert!( + import.library.is_none(), + "Static binary imports should not have library info" + ); } - output.push('\n'); - } - if container_info.exports.len() > 10 { - output.push_str(&format!( - "... and {} more exports\n", - container_info.exports.len() - 10 - )); + println!( + "Static binary: {} imports (all should have library: None)", + container_info.imports.len() + ); + } + _ => { + println!("Got non-ELF binary, skipping test"); } + } + } + _ => { + println!("Static compilation not available, skipping test"); + } + } +} + +#[test] +#[cfg(target_family = "unix")] +fn test_elf_stripped_binary() { + // Test handling of stripped binaries (symbols removed) + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let c_file = temp_dir.path().join("test_strip.c"); + let elf_file = temp_dir.path().join("test_strip"); + + let c_code = r#" + #include - // Snapshot the output - assert_snapshot!("elf_symbol_extraction", output); + int main() { + printf("Hello\n"); + return 0; + } + "#; + + File::create(&c_file) + .expect("Failed to create C file") + .write_all(c_code.as_bytes()) + .expect("Failed to write C code"); + + // Compile and strip + let mut compile_output = Command::new("x86_64-linux-gnu-gcc") + .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) + .output(); + + if compile_output.is_err() + || !compile_output + .as_ref() + .map(|o| o.status.success()) + .unwrap_or(false) + { + compile_output = Command::new("gcc") + .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) + .output(); + } + + if let Ok(output) = compile_output { + if output.status.success() { + // Strip the binary + let _strip_output = Command::new("strip") + .arg(elf_file.to_str().unwrap()) + .output(); + + let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); + + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(_)) => { + let parser = ElfParser::new(); + // Should handle gracefully even if symbols are stripped + if let Ok(container_info) = parser.parse(&elf_data) { + println!( + "Stripped binary: {} imports, {} exports", + container_info.imports.len(), + container_info.exports.len() + ); + // Stripped binaries may have fewer symbols, but parsing should succeed + } + } + _ => { + println!("Got non-ELF binary, skipping test"); + } } } + } else { + println!("GCC not available, skipping test"); } } From 11b74bbdeae1a50a3a48c5b4e16fc18d81c8557d Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 21:35:03 -0500 Subject: [PATCH 10/13] Refactor ELF integration tests to use fixtures and improve structure - Replaced dynamic C code generation in ELF integration tests with pre-compiled binary fixtures for consistency and reliability. - Updated tests to verify ELF parsing, imports, and exports using the new fixture files. - Added new integration tests for Mach-O and PE formats, ensuring comprehensive coverage across binary formats. - Introduced a README in the fixtures directory to document the purpose and rebuilding instructions for the test binaries. Signed-off-by: UncleSp1d3r --- tests/fixtures/README.md | 45 + tests/fixtures/test_binary.c | 22 + tests/fixtures/test_binary_elf | Bin 0 -> 15872 bytes tests/fixtures/test_binary_macho | Bin 0 -> 33632 bytes tests/fixtures/test_binary_pe.exe | Bin 0 -> 244009 bytes tests/integration_elf.rs | 771 ++++++------------ tests/integration_macho.rs | 111 +++ tests/integration_pe.rs | 118 +++ ...ntegration_elf__elf_symbol_extraction.snap | 73 +- 9 files changed, 574 insertions(+), 566 deletions(-) create mode 100644 tests/fixtures/README.md create mode 100644 tests/fixtures/test_binary.c create mode 100755 tests/fixtures/test_binary_elf create mode 100755 tests/fixtures/test_binary_macho create mode 100755 tests/fixtures/test_binary_pe.exe create mode 100644 tests/integration_macho.rs create mode 100644 tests/integration_pe.rs diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..69ef340 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,45 @@ +# Test Fixtures + +This directory contains pre-compiled binary test fixtures used for snapshot testing. + +## Fixtures + +- `test_binary_elf` - x86-64 ELF binary +- `test_binary_macho` - ARM64 Mach-O binary +- `test_binary_pe.exe` - x86-64 PE binary + +## Source + +All fixtures are compiled from `test_binary.c`, a simple C program with: + +- Exported functions: `exported_function`, `helper_function` +- Imports from libc: `printf`, `malloc`, `free` +- A `main` function + +## Rebuilding Fixtures + +If you need to rebuild the fixtures: + +### ELF (x86-64) + +```bash +docker run --rm -v "$(pwd):/work" -w /work --platform linux/amd64 gcc:latest gcc -o test_binary_elf test_binary.c +``` + +### Mach-O (ARM64) + +```bash +clang -o test_binary_macho test_binary.c +``` + +### PE (x86-64) + +```bash +docker run --rm -v "$(pwd):/work" -w /work mcr.microsoft.com/devcontainers/cpp:latest bash -c "apt-get update -qq && apt-get install -y -qq mingw-w64 && x86_64-w64-mingw32-gcc -o test_binary_pe.exe test_binary.c" +``` + +## Notes + +- These fixtures are checked into git to ensure consistent test results +- The fixtures should not be modified unless the test requirements change +- If you modify `test_binary.c`, rebuild all fixtures and update snapshots diff --git a/tests/fixtures/test_binary.c b/tests/fixtures/test_binary.c new file mode 100644 index 0000000..6294553 --- /dev/null +++ b/tests/fixtures/test_binary.c @@ -0,0 +1,22 @@ +#include +#include + +// Export a function +int exported_function(int x) { + return x * 2; +} + +// Another exported function +void helper_function(void) { + printf("Helper called\n"); +} + +// Use some imports +int main() { + printf("Hello, world!\n"); // Import from libc + void* ptr = malloc(100); // Import from libc + free(ptr); // Import from libc + exported_function(42); + return 0; +} + diff --git a/tests/fixtures/test_binary_elf b/tests/fixtures/test_binary_elf new file mode 100755 index 0000000000000000000000000000000000000000..0fdf9692b6316ae82cd0555f0012059c09c72854 GIT binary patch literal 15872 zcmeHOTWlOx89uvC8kbzWNqR9!N;V-yp(2~R71~_JcGekJPEt}A(ua1u-W_LG+KaV2 zrgjlDDBMh5+y_v_OL?eJCB#eBhYAVeHmFfbCDI2_B%~^!6dXa@RJ156RhI8R^M9Mo zbR(5Y@WTAk%zysNcg~sroSpq==A5UpBSXnV!jO_?zY%FHQBiTq+RG>~_zUl+LGdZl2wa}!@@C4&}1Vh@E2?glwp4Ea?xlABa$ z7TjVfs%-T7$bX>?kkhZ#URuDQv|}0h4-`kPX1Axu3WC=jpyMK z^Ijv!)dR1RD^-oia)o{7(8%z>ptmElb62alBeNTj6O~$3As$*vnl2m(JvKqZxWD-H zk8!D8My9dorq+~h7ymD#EOhaJW4XkGKLJr(YSVMhIm|>BYa_ibu!q&Iifn1^3i%y3e*E z(%k(TOwD^=$GK;{gObQav0u5=1l{|J$dZy@xnSjAiYzJpm3he9-yWWQ<8gQP&+e(q zS05V9ZlB+N-aU8DUx}*j{2E-}pUCvA|BlpW>p$Br9yI2q%}C_keR2*~za(DL&D+n2 zqIqdz?(9T6z%jqJ{@;Gbve~Qd%kLa;U%uAmCSG;lxE`!*)v!csh~odz^SKXC?puzI znW@!}xU+99Jc+A#b@9LOAd`6P?8n*@|G)iE9B04z(dq0*iTTIQigadC+A#}nUT-$% zLInbyIiX#w5AdWyBfj9zj1mXz95r`uY zM<9;Ce=h~% z!S!ZMb`WkeTNfIkrUow94$aJaYrEk1x0OdR+C#_ggD7u6TU3@?Nzo{C~!{emvS> zpN{X4@~Mum}J5fc8} z`V^(b2uLS=0s1t49laVi;x^vGe<$=Q_%qJ*zhC^5%M9-y)H}AGrDjO)EA|}?jK@?z zs(Slw2KsTWKiZ!f^l4Vw#=iyq%H-YHuHm$4jX5oP>|%kz>gD~6&`+T66bIQClEVDi z7OCe2@lP%_b1BE-AzG3ZF{?0t##Fyi^{;6?(fIs9{bgZ{kbTz#{zvGg|55+HihmM! zA+6tnbn5tA}0BMsv zcm_AQ27V*(#!J;)eFoj~yrE+UUUfsuzj9lPLtwAa8O_l)n#v2W7#%|B>ywSsh43dP@m`r1)66D6ggL=q|ES0Jl z=1G$|QJu;ZbB&_O6lSV$4|z}zEnoNR4Y^3`rh*3>I+PQDmL|)A$=D_{fj^D13_ff% z8@Y^M)Zs1`fTd(I)mq@&nkVZuykQPzRMkH)yhWy_tumY#VU;YSLAX#d%tM^O+P@yxfTBY3tSz5cx72ed-wZ_g`U{`Z9h ze;1A@z5<=ZJNbH5|^L8j@bJc@q-`gT0?QtpS0;-mea z2VQa(&QIor+;13dk23k|!1lD^TX`tfn+nk{ly1n>UxY#CF572*$o-?-C(3>k$9eQ8 zU?u*k8)C;LvU@C!Rb Z>1{To#)yay2p}Swe*^yf+!_D? literal 0 HcmV?d00001 diff --git a/tests/fixtures/test_binary_macho b/tests/fixtures/test_binary_macho new file mode 100755 index 0000000000000000000000000000000000000000..939dd141dc462a043fb098f1a9ac028f85f42d9a GIT binary patch literal 33632 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'nw@{pV%lUx@NWp>+={i{HI#}sC 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⩔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 literal 0 HcmV?d00001 diff --git a/tests/integration_elf.rs b/tests/integration_elf.rs index d0e1c03..6d36813 100644 --- a/tests/integration_elf.rs +++ b/tests/integration_elf.rs @@ -1,274 +1,127 @@ use insta::assert_snapshot; use std::fs; -use std::fs::File; -use std::io::Write; -use std::process::Command; use stringy::container::{ContainerParser, ElfParser}; -use tempfile::TempDir; -#[test] -#[cfg(target_family = "unix")] -fn test_elf_import_export_extraction_dynamic() { - // Create a simple C program that we can compile to test with - let c_code = r#" -#include -#include - -// Export a function -int exported_function(int x) { - return x * 2; +fn get_fixture_path(name: &str) -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(name) } -// Use some imports -int main() { - printf("Hello, world!\n"); // Import from libc - void* ptr = malloc(100); // Import from libc - free(ptr); // Import from libc - return 0; -} -"#; - - // Write the C code to a temporary file - let temp_dir = std::env::temp_dir(); - let c_file = temp_dir.join("test_elf.c"); - let elf_file = temp_dir.join("test_elf"); - - fs::write(&c_file, c_code).expect("Failed to write C file"); - - // Try to compile it with gcc, attempting to force ELF output - // First try with a cross-compiler for Linux if available - // NOTE: This is for dynamic linking test, so we DON'T use -static - let mut output = Command::new("x86_64-linux-gnu-gcc") - .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) - .output(); - - // If cross-compiler not available, try regular gcc (dynamically linked) - if output.is_err() { - output = Command::new("gcc") - .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) - .output(); - } - - match output { - Ok(result) if result.status.success() => { - // Successfully compiled, now test our ELF parser - let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); - - // Check what format we actually got - match goblin::Object::parse(&elf_data) { - Ok(goblin::Object::Elf(_)) => { - // Great! We have an ELF binary, test our parser - assert!(ElfParser::detect(&elf_data), "ELF detection should succeed"); - } - Ok(goblin::Object::Mach(_)) => { - println!("Got Mach-O binary (expected on macOS), skipping ELF-specific test"); - // Clean up and return early - let _ = fs::remove_file(&c_file); - let _ = fs::remove_file(&elf_file); - return; - } - Ok(other) => { - println!( - "Got unexpected binary format: {:?}, skipping test", - std::mem::discriminant(&other) - ); - let _ = fs::remove_file(&c_file); - let _ = fs::remove_file(&elf_file); - return; - } - Err(e) => { - println!("Failed to parse binary: {}, skipping test", e); - let _ = fs::remove_file(&c_file); - let _ = fs::remove_file(&elf_file); - return; - } - } - - // Test parsing - let parser = ElfParser::new(); - let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - - // Verify we found some imports - assert!( - !container_info.imports.is_empty(), - "Should find imports like printf, malloc, free" - ); - - // Check that we found expected imports - let import_names: Vec<&str> = container_info - .imports - .iter() - .map(|imp| imp.name.as_str()) - .collect(); - - // We should find at least some of these common libc functions - let expected_imports = ["printf", "malloc", "free", "__libc_start_main"]; - let found_expected = expected_imports - .iter() - .any(|&expected| import_names.contains(&expected)); - - assert!( - found_expected, - "Should find at least one expected import. Found: {:?}", - import_names - ); - - // Verify we found some exports (at least main and our exported function) - // Note: exports might be stripped in some builds, so we'll be lenient - println!( - "Found {} imports and {} exports", - container_info.imports.len(), - container_info.exports.len() - ); - - // Clean up - let _ = fs::remove_file(&c_file); - let _ = fs::remove_file(&elf_file); - } - Ok(_) => { - println!("gcc compilation failed, skipping ELF integration test"); - // This is not a test failure - just means gcc isn't available - } - Err(_) => { - println!("gcc not found, skipping ELF integration test"); - // This is not a test failure - just means gcc isn't available - } - } +#[test] +fn test_elf_import_export_extraction_dynamic() { + // Test with the ELF fixture + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + // Verify it's an ELF file + assert!(ElfParser::detect(&elf_data), "ELF detection should succeed"); + + // Test parsing + let parser = ElfParser::new(); + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); + + // Verify we found some imports + assert!( + !container_info.imports.is_empty(), + "Should find imports like printf, malloc, free" + ); + + // Check that we found expected imports + let import_names: Vec<&str> = container_info + .imports + .iter() + .map(|imp| imp.name.as_str()) + .collect(); + + // We should find at least some of these common libc functions + let expected_imports = ["malloc", "free", "__libc_start_main"]; + let found_expected = expected_imports + .iter() + .any(|&expected| import_names.iter().any(|&name| name.contains(expected))); + + assert!( + found_expected, + "Should find at least one expected import. Found: {:?}", + import_names + ); + + // Verify we found some exports (at least main and our exported function) + let export_names: Vec<&str> = container_info + .exports + .iter() + .map(|exp| exp.name.as_str()) + .collect(); + + assert!( + export_names.contains(&"main"), + "Should find main export. Found: {:?}", + export_names + ); + assert!( + export_names.contains(&"exported_function"), + "Should find exported_function export. Found: {:?}", + export_names + ); + + println!( + "Found {} imports and {} exports", + container_info.imports.len(), + container_info.exports.len() + ); } #[test] -#[cfg(target_family = "unix")] fn test_elf_import_export_extraction_static() { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let c_file = temp_dir.path().join("test_static.c"); - let elf_file = temp_dir.path().join("test_static"); - - let c_code = r#" - #include - #include - - void exported_function() { - printf("Hello from exported function\n"); - } - - int main() { - void *ptr = malloc(100); - printf("Allocated memory\n"); - free(ptr); - exported_function(); - return 0; - } - "#; - - File::create(&c_file) - .expect("Failed to create C file") - .write_all(c_code.as_bytes()) - .expect("Failed to write C code"); - - // Compile statically-linked binary with -static flag - let mut output = Command::new("x86_64-linux-gnu-gcc") - .args([ - "-static", - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) - .output(); - - if output.is_err() || !output.as_ref().map(|o| o.status.success()).unwrap_or(false) { - output = Command::new("gcc") - .args([ - "-static", - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) - .output(); - } - - match output { - Ok(output) if output.status.success() => { - let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); - - let format_obj = goblin::Object::parse(&elf_data).expect("Failed to parse with goblin"); - - match format_obj { - goblin::Object::Elf(_elf) => { - let parser = ElfParser::new(); - let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - - // Statically-linked binaries typically have no or very few dynamic imports - // since all dependencies are embedded - println!( - "Static binary imports found: {} (expected: 0 or very few)", - container_info.imports.len() - ); - - // Check exports - note that static binaries may have symbols stripped - // or may not expose them depending on compilation flags - let export_names: Vec = container_info - .exports - .iter() - .map(|e| e.name.clone()) - .collect(); - - println!( - "Static binary exports found: {} exports: {:?}", - container_info.exports.len(), - export_names - ); - - // If exports are present, verify expected ones exist - // Note: Exports may be stripped in static binaries, so this is not always guaranteed - if !container_info.exports.is_empty() { - let has_main = export_names.iter().any(|name| name == "main"); - let has_exported_function = - export_names.iter().any(|name| name == "exported_function"); - - if has_main || has_exported_function { - println!( - "Found expected exports: main={}, exported_function={}", - has_main, has_exported_function - ); - } - } else { - println!( - "No exports found in static binary. This can happen when symbols are stripped or not exported." - ); - } - } - goblin::Object::Mach(_) => { - println!("Compiled to Mach-O, skipping ELF-specific test"); - } - _ => panic!("Unexpected binary format"), - } - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!( - "Static compilation failed, skipping test. This is expected if static libraries are not available.\nError: {}", - stderr - ); - } - Err(e) => { - println!( - "GCC not available, skipping test. This is expected in some CI environments. Error: {}", - e - ); - } - } + // Test with the ELF fixture (dynamically linked, but we can still test parsing) + // Note: For true static binary testing, we'd need a separate static fixture + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + let parser = ElfParser::new(); + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); + + // Our fixture is dynamically linked, so it should have imports + println!("Binary imports found: {}", container_info.imports.len()); + + // Check exports + let export_names: Vec = container_info + .exports + .iter() + .map(|e| e.name.clone()) + .collect(); + + println!( + "Binary exports found: {} exports: {:?}", + container_info.exports.len(), + export_names + ); + + // Verify expected exports exist + assert!( + export_names.contains(&"main".to_string()), + "Should find main export" + ); + assert!( + export_names.contains(&"exported_function".to_string()), + "Should find exported_function export" + ); } #[test] -#[cfg(target_family = "unix")] fn test_elf_section_classification_integration() { - // Test with the current binary (this test executable) - let current_exe = std::env::current_exe().expect("Failed to get current executable path"); - - if let Ok(elf_data) = fs::read(¤t_exe) - && ElfParser::detect(&elf_data) - && let Ok(container_info) = ElfParser::new().parse(&elf_data) - { + // Test with the ELF fixture + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + if ElfParser::detect(&elf_data) { + let container_info = ElfParser::new() + .parse(&elf_data) + .expect("Failed to parse ELF fixture"); // Verify we found sections and classified them assert!( !container_info.sections.is_empty(), @@ -330,74 +183,74 @@ fn test_elf_section_classification_integration() { has_text || has_rodata, "Should find .text or .rodata sections" ); + } else { + panic!("ELF fixture is not a valid ELF file"); } } #[test] -#[cfg(target_family = "unix")] fn test_elf_library_dependencies() { - // Test with the current binary (this test executable) which should have library dependencies - let current_exe = std::env::current_exe().expect("Failed to get current executable path"); - - if let Ok(elf_data) = fs::read(¤t_exe) { - // Parse with goblin to check if it's ELF - match goblin::Object::parse(&elf_data) { - Ok(goblin::Object::Elf(elf)) => { - // Check if we have a dynamic section - if let Some(ref dynamic) = elf.dynamic { - // Extract libraries using the method we're testing - let libraries = dynamic.get_libraries(&elf.dynstrtab); - - println!("Found {} library dependencies:", libraries.len()); - for lib in &libraries { - println!(" - {}", lib); - } + // Test with the ELF fixture + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + // Parse with goblin to check if it's ELF + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(elf)) => { + // Check if we have a dynamic section + if let Some(ref dynamic) = elf.dynamic { + // Extract libraries using the method we're testing + let libraries = dynamic.get_libraries(&elf.dynstrtab); + + println!("Found {} library dependencies:", libraries.len()); + for lib in &libraries { + println!(" - {}", lib); + } - // A dynamically linked ELF binary should typically have at least one library - // (e.g., libc.so.6 on Linux) - // But we'll be lenient here since we might be on a different platform - if !libraries.is_empty() { - // Verify at least one common library is present - let has_libc = libraries.iter().any(|lib| lib.contains("libc")); - let has_libpthread = libraries.iter().any(|lib| lib.contains("pthread")); - let has_libm = libraries.iter().any(|lib| lib.contains("libm")); - - // At least one common library should be present in a typical executable - if has_libc || has_libpthread || has_libm { - println!("✓ Found expected library dependencies"); - } - } else { - println!( - "No library dependencies found. This might be a static binary or on a non-Linux platform." - ); + // A dynamically linked ELF binary should typically have at least one library + // (e.g., libc.so.6 on Linux) + // But we'll be lenient here since we might be on a different platform + if !libraries.is_empty() { + // Verify at least one common library is present + let has_libc = libraries.iter().any(|lib| lib.contains("libc")); + let has_libpthread = libraries.iter().any(|lib| lib.contains("pthread")); + let has_libm = libraries.iter().any(|lib| lib.contains("libm")); + + // At least one common library should be present in a typical executable + if has_libc || has_libpthread || has_libm { + println!("✓ Found expected library dependencies"); } } else { - println!("No dynamic section found. This might be a static binary."); + println!( + "No library dependencies found. This might be a static binary or on a non-Linux platform." + ); } + } else { + println!("No dynamic section found. This might be a static binary."); } - Ok(goblin::Object::Mach(_)) => { - println!("Got Mach-O binary (expected on macOS), skipping ELF library test"); - } - Ok(_) => { - println!("Got non-ELF binary format, skipping test"); - } - Err(e) => { - println!("Failed to parse binary: {}, skipping test", e); - } + } + Ok(_) => { + panic!("Expected ELF binary from fixture"); + } + Err(e) => { + panic!("Failed to parse ELF fixture: {}", e); } } } #[test] -#[cfg(target_family = "unix")] fn test_elf_symbol_extraction_snapshot() { - // Test with the current binary to create a snapshot of symbol extraction - let current_exe = std::env::current_exe().expect("Failed to get current executable path"); + // Test with a fixed ELF fixture to create a consistent snapshot + let fixture_path = get_fixture_path("test_binary_elf"); - if let Ok(elf_data) = fs::read(¤t_exe) - && ElfParser::detect(&elf_data) - && let Ok(container_info) = ElfParser::new().parse(&elf_data) - { + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + if ElfParser::detect(&elf_data) { + let container_info = ElfParser::new() + .parse(&elf_data) + .expect("Failed to parse ELF fixture"); // Create a formatted output for snapshot testing let mut output = String::new(); @@ -447,123 +300,72 @@ fn test_elf_symbol_extraction_snapshot() { // Snapshot the output assert_snapshot!("elf_symbol_extraction", output); + } else { + panic!("ELF fixture is not a valid ELF file"); } } #[test] -#[cfg(target_family = "unix")] fn test_elf_symbol_library_mapping() { // Test symbol-to-library mapping using version information - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let c_file = temp_dir.path().join("test_versioned.c"); - let elf_file = temp_dir.path().join("test_versioned"); - - let c_code = r#" - #include - #include - - int main() { - printf("Hello from versioned symbol\n"); // Should map to libc - void* ptr = malloc(100); // Should map to libc - free(ptr); - return 0; - } - "#; - - File::create(&c_file) - .expect("Failed to create C file") - .write_all(c_code.as_bytes()) - .expect("Failed to write C code"); - - // Compile dynamically linked binary (version info typically present) - let mut output = Command::new("x86_64-linux-gnu-gcc") - .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) - .output(); - - if output.is_err() || !output.as_ref().map(|o| o.status.success()).unwrap_or(false) { - output = Command::new("gcc") - .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) - .output(); - } - - match output { - Ok(output) if output.status.success() => { - let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); - match goblin::Object::parse(&elf_data) { - Ok(goblin::Object::Elf(_)) => { - let parser = ElfParser::new(); - let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - - // Check that we found imports - assert!(!container_info.imports.is_empty(), "Should find imports"); + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(_)) => { + let parser = ElfParser::new(); + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - // Check that some imports have library information populated - let imports_with_libs: Vec<_> = container_info - .imports - .iter() - .filter(|imp| imp.library.is_some()) - .collect(); + // Check that we found imports + assert!(!container_info.imports.is_empty(), "Should find imports"); - println!( - "Found {} imports with library information out of {} total imports", - imports_with_libs.len(), - container_info.imports.len() - ); + // Check that some imports have library information populated + let imports_with_libs: Vec<_> = container_info + .imports + .iter() + .filter(|imp| imp.library.is_some()) + .collect(); - // Common libc symbols should have library info if version info is available - let printf_import = container_info - .imports - .iter() - .find(|imp| imp.name == "printf"); - let malloc_import = container_info - .imports - .iter() - .find(|imp| imp.name == "malloc"); - - if let Some(printf) = printf_import { - println!("printf import: {:?}", printf); - // If version info is available, library should be populated - // Otherwise, it may be None (unversioned or fallback didn't match) - } + println!( + "Found {} imports with library information out of {} total imports", + imports_with_libs.len(), + container_info.imports.len() + ); - if let Some(malloc) = malloc_import { - println!("malloc import: {:?}", malloc); - } + // Common libc symbols should have library info if version info is available + let malloc_import = container_info + .imports + .iter() + .find(|imp| imp.name.contains("malloc")); - // At least verify the mapping logic runs without errors - // Actual library attribution depends on binary's version info - } - Ok(goblin::Object::Mach(_)) => { - println!("Got Mach-O binary, skipping ELF-specific test"); - } - Ok(_) => { - println!("Got non-ELF binary, skipping test"); - } - Err(e) => { - println!("Failed to parse binary: {}, skipping test", e); - } + if let Some(malloc) = malloc_import { + println!("malloc import: {:?}", malloc); } + + // At least verify the mapping logic runs without errors + // Actual library attribution depends on binary's version info } Ok(_) => { - println!("Compilation failed, skipping test"); + panic!("Expected ELF binary from fixture"); } - Err(_) => { - println!("GCC not available, skipping test"); + Err(e) => { + panic!("Failed to parse ELF fixture: {}", e); } } } #[test] -#[cfg(target_family = "unix")] fn test_elf_unversioned_symbols() { // Test handling of symbols without version info - let current_exe = std::env::current_exe().expect("Failed to get current executable path"); - - if let Ok(elf_data) = fs::read(¤t_exe) - && ElfParser::detect(&elf_data) - && let Ok(container_info) = ElfParser::new().parse(&elf_data) - { + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + if ElfParser::detect(&elf_data) { + let container_info = ElfParser::new() + .parse(&elf_data) + .expect("Failed to parse ELF fixture"); // Count imports with and without library info let with_lib = container_info .imports @@ -587,149 +389,60 @@ fn test_elf_unversioned_symbols() { !container_info.imports.is_empty(), "Should find at least some imports" ); + } else { + panic!("ELF fixture is not a valid ELF file"); } } #[test] -#[cfg(target_family = "unix")] fn test_elf_no_dynamic_section() { - // Test static binaries (no dynamic section) - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let c_file = temp_dir.path().join("test_static.c"); - let elf_file = temp_dir.path().join("test_static"); - - let c_code = r#" - int main() { - return 0; - } - "#; - - File::create(&c_file) - .expect("Failed to create C file") - .write_all(c_code.as_bytes()) - .expect("Failed to write C code"); - - // Try to compile statically - let mut output = Command::new("x86_64-linux-gnu-gcc") - .args([ - "-static", - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) - .output(); - - if output.is_err() || !output.as_ref().map(|o| o.status.success()).unwrap_or(false) { - output = Command::new("gcc") - .args([ - "-static", - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) - .output(); - } + // Test with the ELF fixture (dynamically linked, but we can test parsing) + // Note: For true static binary testing, we'd need a separate static fixture + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(_)) => { + let parser = ElfParser::new(); + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - match output { - Ok(output) if output.status.success() => { - let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); - - match goblin::Object::parse(&elf_data) { - Ok(goblin::Object::Elf(_)) => { - let parser = ElfParser::new(); - let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - - // Static binaries should have no or very few imports - // and those imports should have library: None - for import in &container_info.imports { - assert!( - import.library.is_none(), - "Static binary imports should not have library info" - ); - } + // Our fixture is dynamically linked, so it should have imports + // Some may have library info if version info is available + println!("Binary: {} imports", container_info.imports.len()); - println!( - "Static binary: {} imports (all should have library: None)", - container_info.imports.len() - ); - } - _ => { - println!("Got non-ELF binary, skipping test"); - } - } + // Verify parsing works correctly + assert!(!container_info.sections.is_empty(), "Should have sections"); } _ => { - println!("Static compilation not available, skipping test"); + panic!("Expected ELF binary from fixture"); } } } #[test] -#[cfg(target_family = "unix")] fn test_elf_stripped_binary() { - // Test handling of stripped binaries (symbols removed) - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let c_file = temp_dir.path().join("test_strip.c"); - let elf_file = temp_dir.path().join("test_strip"); - - let c_code = r#" - #include - - int main() { - printf("Hello\n"); - return 0; + // Test with the ELF fixture (not stripped, but we can test parsing) + // Note: For true stripped binary testing, we'd need a separate stripped fixture + let fixture_path = get_fixture_path("test_binary_elf"); + let elf_data = fs::read(&fixture_path) + .expect("Failed to read ELF fixture. Run the build script to generate fixtures."); + + match goblin::Object::parse(&elf_data) { + Ok(goblin::Object::Elf(_)) => { + let parser = ElfParser::new(); + // Should handle gracefully + let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); + println!( + "Binary: {} imports, {} exports", + container_info.imports.len(), + container_info.exports.len() + ); + // Parsing should succeed + assert!(!container_info.sections.is_empty(), "Should have sections"); } - "#; - - File::create(&c_file) - .expect("Failed to create C file") - .write_all(c_code.as_bytes()) - .expect("Failed to write C code"); - - // Compile and strip - let mut compile_output = Command::new("x86_64-linux-gnu-gcc") - .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) - .output(); - - if compile_output.is_err() - || !compile_output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { - compile_output = Command::new("gcc") - .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) - .output(); - } - - if let Ok(output) = compile_output { - if output.status.success() { - // Strip the binary - let _strip_output = Command::new("strip") - .arg(elf_file.to_str().unwrap()) - .output(); - - let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); - - match goblin::Object::parse(&elf_data) { - Ok(goblin::Object::Elf(_)) => { - let parser = ElfParser::new(); - // Should handle gracefully even if symbols are stripped - if let Ok(container_info) = parser.parse(&elf_data) { - println!( - "Stripped binary: {} imports, {} exports", - container_info.imports.len(), - container_info.exports.len() - ); - // Stripped binaries may have fewer symbols, but parsing should succeed - } - } - _ => { - println!("Got non-ELF binary, skipping test"); - } - } + _ => { + panic!("Expected ELF binary from fixture"); } - } else { - println!("GCC not available, skipping test"); } } diff --git a/tests/integration_macho.rs b/tests/integration_macho.rs new file mode 100644 index 0000000..fac959d --- /dev/null +++ b/tests/integration_macho.rs @@ -0,0 +1,111 @@ +use std::fs; +use stringy::container::{ContainerParser, MachoParser}; + +fn get_fixture_path(name: &str) -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(name) +} + +#[test] +fn test_macho_import_export_extraction() { + // Test with the Mach-O fixture + let fixture_path = get_fixture_path("test_binary_macho"); + let macho_data = fs::read(&fixture_path) + .expect("Failed to read Mach-O fixture. Run the build script to generate fixtures."); + + // Verify it's a Mach-O file + assert!( + MachoParser::detect(&macho_data), + "Mach-O detection should succeed" + ); + + // Test parsing + let parser = MachoParser::new(); + let container_info = parser.parse(&macho_data).expect("Failed to parse Mach-O"); + + // Verify we found some sections + assert!( + !container_info.sections.is_empty(), + "Should find sections in Mach-O binary" + ); + + // Check exports + let export_names: Vec<&str> = container_info + .exports + .iter() + .map(|exp| exp.name.as_str()) + .collect(); + + assert!( + export_names + .iter() + .any(|&name| name == "main" || name == "_main"), + "Should find main export. Found: {:?}", + export_names + ); + assert!( + export_names + .iter() + .any(|&name| name == "exported_function" || name == "_exported_function"), + "Should find exported_function export. Found: {:?}", + export_names + ); + + println!( + "Found {} imports and {} exports", + container_info.imports.len(), + container_info.exports.len() + ); +} + +#[test] +fn test_macho_section_classification() { + // Test with the Mach-O fixture + let fixture_path = get_fixture_path("test_binary_macho"); + let macho_data = fs::read(&fixture_path) + .expect("Failed to read Mach-O fixture. Run the build script to generate fixtures."); + + if MachoParser::detect(&macho_data) { + let container_info = MachoParser::new() + .parse(&macho_data) + .expect("Failed to parse Mach-O fixture"); + + // Verify we found sections and classified them + assert!( + !container_info.sections.is_empty(), + "Should find sections in Mach-O binary" + ); + + // Verify that all sections have weights assigned + for section in &container_info.sections { + assert!( + section.weight > 0.0, + "Section {} should have a positive weight, got {}", + section.name, + section.weight + ); + } + + // Look for common Mach-O sections + let section_names: Vec<&str> = container_info + .sections + .iter() + .map(|sec| sec.name.as_str()) + .collect(); + + println!("Found sections: {:?}", section_names); + + // Should find at least some standard Mach-O sections + let has_text = section_names.iter().any(|&name| name.contains("__TEXT")); + let has_data = section_names.iter().any(|&name| name.contains("__DATA")); + + assert!( + has_text || has_data, + "Should find __TEXT or __DATA sections" + ); + } else { + panic!("Mach-O fixture is not a valid Mach-O file"); + } +} diff --git a/tests/integration_pe.rs b/tests/integration_pe.rs new file mode 100644 index 0000000..8248a33 --- /dev/null +++ b/tests/integration_pe.rs @@ -0,0 +1,118 @@ +use std::fs; +use stringy::container::{ContainerParser, PeParser}; + +fn get_fixture_path(name: &str) -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(name) +} + +#[test] +fn test_pe_import_export_extraction() { + // Test with the PE fixture + let fixture_path = get_fixture_path("test_binary_pe.exe"); + let pe_data = fs::read(&fixture_path) + .expect("Failed to read PE fixture. Run the build script to generate fixtures."); + + // Verify it's a PE file + assert!(PeParser::detect(&pe_data), "PE detection should succeed"); + + // Test parsing + let parser = PeParser::new(); + let container_info = parser.parse(&pe_data).expect("Failed to parse PE"); + + // Verify we found some sections + assert!( + !container_info.sections.is_empty(), + "Should find sections in PE binary" + ); + + // Check exports (PE executables may not have exports, only DLLs typically do) + let export_names: Vec<&str> = container_info + .exports + .iter() + .map(|exp| exp.name.as_str()) + .collect(); + + println!("PE exports found: {:?}", export_names); + + // PE executables typically don't export symbols (only DLLs do) + // So we just verify parsing works and sections are found + if !export_names.is_empty() { + // If exports are present, check for expected ones + let has_main = export_names + .iter() + .any(|&name| name == "main" || name.contains("main")); + let has_exported = export_names + .iter() + .any(|&name| name == "exported_function" || name.contains("exported_function")); + + if has_main || has_exported { + println!( + "Found expected exports: main={}, exported_function={}", + has_main, has_exported + ); + } + } else { + println!("No exports found (expected for PE executables, only DLLs export symbols)"); + } + + println!( + "Found {} imports and {} exports", + container_info.imports.len(), + container_info.exports.len() + ); +} + +#[test] +fn test_pe_section_classification() { + // Test with the PE fixture + let fixture_path = get_fixture_path("test_binary_pe.exe"); + let pe_data = fs::read(&fixture_path) + .expect("Failed to read PE fixture. Run the build script to generate fixtures."); + + if PeParser::detect(&pe_data) { + let container_info = PeParser::new() + .parse(&pe_data) + .expect("Failed to parse PE fixture"); + + // Verify we found sections and classified them + assert!( + !container_info.sections.is_empty(), + "Should find sections in PE binary" + ); + + // Verify that all sections have weights assigned + for section in &container_info.sections { + assert!( + section.weight > 0.0, + "Section {} should have a positive weight, got {}", + section.name, + section.weight + ); + } + + // Look for common PE sections + let section_names: Vec<&str> = container_info + .sections + .iter() + .map(|sec| sec.name.as_str()) + .collect(); + + println!("Found sections: {:?}", section_names); + + // Should find at least some standard PE sections + let has_text = section_names.iter().any(|&name| name.contains(".text")); + let has_data = section_names + .iter() + .any(|&name| name.contains(".data") || name.contains(".rdata")); + + assert!( + has_text || has_data, + "Should find .text or .data/.rdata sections" + ); + } else { + panic!("PE fixture is not a valid PE file"); + } +} diff --git a/tests/snapshots/integration_elf__elf_symbol_extraction.snap b/tests/snapshots/integration_elf__elf_symbol_extraction.snap index 8769a43..f687f84 100644 --- a/tests/snapshots/integration_elf__elf_symbol_extraction.snap +++ b/tests/snapshots/integration_elf__elf_symbol_extraction.snap @@ -3,61 +3,60 @@ source: tests/integration_elf.rs expression: output --- === IMPORTS === -Total: 134 +Total: 9 -Import 1: __libc_start_main +Import 1: free + Library: libc.so.6 -Import 2: __gmon_start__ +Import 2: __libc_start_main + Library: libc.so.6 -Import 3: _ITM_deregisterTMCloneTable +Import 3: puts + Library: libc.so.6 -Import 4: _ITM_registerTMCloneTable +Import 4: __gmon_start__ + Library: libc.so.6 -Import 5: __cxa_finalize +Import 5: malloc + Library: libc.so.6 -Import 6: _Unwind_Resume +Import 6: free@GLIBC_2.2.5 -Import 7: memcpy +Import 7: __libc_start_main@GLIBC_2.34 -Import 8: memset +Import 8: puts@GLIBC_2.2.5 -Import 9: memcmp - -Import 10: bcmp - -... and 124 more imports +Import 9: malloc@GLIBC_2.2.5 === EXPORTS === -Total: 7362 - -Export 1: _start - Address: 0xed9e0 +Total: 10 -Export 2: main - Address: 0xf7f90 +Export 1: data_start + Address: 0x404018 -Export 3: _RNvCsj3IbkTTFM3W_7___rustc10rust_panic - Address: 0x363330 +Export 2: _edata + Address: 0x404028 -Export 4: _RNvCsj3IbkTTFM3W_7___rustc17___rust_drop_panic - Address: 0x3634d0 +Export 3: exported_function + Address: 0x401146 -Export 5: _RNvCsj3IbkTTFM3W_7___rustc17rust_begin_unwind - Address: 0x3635a0 +Export 4: helper_function + Address: 0x401154 -Export 6: _RNvCsj3IbkTTFM3W_7___rustc24___rust_foreign_exception - Address: 0x363650 +Export 5: __data_start + Address: 0x404018 -Export 7: _ZN3std3sys4args4unix3imp15ARGV_INIT_ARRAY17h53e3ae54f15b3a00E - Address: 0x3c3030 +Export 6: _IO_stdin_used + Address: 0x402000 -Export 8: rust_eh_personality - Address: 0x3a8060 +Export 7: _end + Address: 0x404030 -Export 9: _RNvCsj3IbkTTFM3W_7___rustc18___rust_start_panic - Address: 0x3a8620 +Export 8: _start + Address: 0x401060 -Export 10: _RNvCsj3IbkTTFM3W_7___rustc20___rust_panic_cleanup - Address: 0x3a86d0 +Export 9: __bss_start + Address: 0x404028 -... and 7352 more exports +Export 10: main + Address: 0x401165 From 280862b9757f838d9b14902334f9ecad6268bcb9 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Sun, 9 Nov 2025 21:49:03 -0500 Subject: [PATCH 11/13] Refactor GitHub Actions workflow for documentation build - Updated the GitHub Actions workflow to streamline the rustdoc build process by removing unnecessary target directory specification. - Ensured the rustdoc output is correctly copied to the documentation directory after building. - Improved overall clarity and efficiency of the documentation build steps. Signed-off-by: UncleSp1d3r --- .github/workflows/docs.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a258290..497a327 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,17 +47,17 @@ jobs: - name: Install mdbook plugins run: cargo binstall mdbook-tabs mdbook-i18n-helpers mdbook-alerts mdbook-yml-header mdbook-image-size --no-confirm - - name: Build rustdoc - run: | - cargo doc --no-deps --document-private-items --target-dir target - mkdir -p docs/book/api - cp -r target/doc/* docs/book/api/ - - name: Build mdBook run: | cd docs mdbook build + - name: Build rustdoc + run: | + cargo doc --no-deps --document-private-items + mkdir -p docs/book/api + cp -r target/doc/* docs/book/api/ + - name: Setup Pages if: github.ref == 'refs/heads/main' uses: actions/configure-pages@v5 From 3b86f6a669dc260a3c250bb428b15b7ce8d87a5d Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Mon, 10 Nov 2025 00:44:08 -0500 Subject: [PATCH 12/13] Update docs workflow --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 497a327..81a62ae 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,7 +36,7 @@ jobs: uses: jontze/action-mdbook@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - mdbook-version: latest + mdbook-version: 0.4.52 use-mermaid: true use-toc: true use-admonish: true From 321024ba93832ebbfa8ecaaffb1b6f7e98287dcd Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Mon, 10 Nov 2025 00:46:21 -0500 Subject: [PATCH 13/13] Update dependencies in Cargo.toml - Bumped `clap` version from 4.5.48 to 4.5.51. - Updated `goblin` from 0.10.1 to 0.10.3. - Upgraded `insta` from 1.0 to 1.43. - Increased `tempfile` version from 3.8 to 3.23. Signed-off-by: UncleSp1d3r --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 21bf169..c8d5da9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,16 +19,16 @@ name = "stringy" path = "src/main.rs" [dependencies] -clap = { version = "4.5.48", features = ["derive"] } -goblin = "0.10.1" +clap = { version = "4.5.51", features = ["derive"] } +goblin = "0.10.3" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0" thiserror = "2.0.17" [dev-dependencies] criterion = "0.7.0" -insta = "1.0" -tempfile = "3.8" +insta = "1.43" +tempfile = "3.23" # The profile that 'dist' will build with [profile.dist]