This commit is contained in:
Christopher Williams 2025-08-20 21:41:40 -04:00
parent 14e8a32f35
commit e18612fd4a
3 changed files with 98 additions and 124 deletions

View File

@ -1,99 +0,0 @@
# Code Refactoring Summary
## Overview
The `parser.rs` file has been refactored from a single 600+ line file into a more maintainable, modular structure. The refactoring improves code organization, readability, and testability while maintaining all existing functionality.
## New Module Structure
### `asn1_types.rs`
- **Purpose**: Core ASN.1 type definitions and basic parsing
- **Key Types**: `TagClass`, `ParentCtx`, `TlvInfo`
- **Key Functions**: `parse_tlv()`, `looks_like_single_der()`, `as_i128()`, `decode_oid()`, `ip_to_string()`
- **Tests**: Basic TLV parsing, OID decoding, IP address parsing
### `encoding_utils.rs`
- **Purpose**: Hex encoding/decoding and bit string formatting utilities
- **Key Types**: `BitsFormat`, `BitsDisplay`
- **Key Functions**: `hex_to_bytes()`, `to_hex_upper()`, `bits_to_string_truncated()`, `format_bitstring_exact()`
- **Tests**: Hex conversion, bit string formatting, truncation logic
### `oid_registry.rs`
- **Purpose**: Object Identifier (OID) to human-readable name mappings
- **Key Functions**: `oid_friendly()` - maps OIDs to descriptive names
- **Content**: Comprehensive registry of X.509, cryptographic, and extension OIDs
### `time_utils.rs`
- **Purpose**: ASN.1 time format parsing and human-readable conversion
- **Key Functions**: `human_utctime()`, `human_generalizedtime()`
- **Features**: Converts ASN.1 time formats to readable ISO format
### `certificate_utils.rs`
- **Purpose**: X.509 certificate-specific parsing utilities
- **Key Functions**: `decode_keyusage_from_octet()`, `try_decode_inner_seq()`
- **Features**: Key usage bit field interpretation, sequence heuristics
### `value_printer.rs`
- **Purpose**: ASN.1 value formatting and display logic
- **Key Types**: `PrintCtx`
- **Key Functions**: `print_primitive_value_with_label()`, individual type printers
- **Features**: Colored output, context-aware labeling
### `pretty_printer.rs`
- **Purpose**: Main DER pretty-printing orchestration
- **Key Functions**: `pretty_print_der()` - main recursive printer
- **Features**: Constructed type handling, label determination, context tracking
### `parser.rs` (simplified)
- **Purpose**: Public API and integration point
- **Key Functions**: `parse_and_print()` - main entry point
- **Content**: Re-exports and integration tests
## Key Improvements
### 1. **Separation of Concerns**
- Each module has a single, well-defined responsibility
- Clear boundaries between parsing, formatting, and display logic
- Certificate-specific code isolated from generic ASN.1 handling
### 2. **Improved Testability**
- Each module can be tested independently
- Tests moved to their appropriate modules
- Better test coverage through focused unit tests
### 3. **Enhanced Maintainability**
- Smaller, focused files are easier to understand and modify
- Related functionality grouped together
- Clear module dependencies
### 4. **Better Code Organization**
- Type definitions centralized in `asn1_types`
- Utility functions properly categorized
- Configuration and display logic separated
### 5. **Reduced Coupling**
- Modules depend on well-defined interfaces
- Functionality can be reused across modules
- Easier to add new features without touching existing code
## Backward Compatibility
- All existing public APIs maintained
- All tests continue to pass
- No functional changes to the application behavior
- Same command-line interface and output format
## Benefits for Future Development
1. **Easier Feature Addition**: New ASN.1 types can be added by extending the appropriate modules
2. **Improved Debugging**: Issues can be isolated to specific modules
3. **Better Documentation**: Each module can be documented independently
4. **Code Reuse**: Utility functions are more discoverable and reusable
5. **Performance Optimization**: Individual modules can be optimized without affecting others
## Testing
All existing tests pass, and new module-specific tests have been added:
- `cargo test` runs 14 unit tests across multiple modules
- Integration tests verify end-to-end functionality
- No regression in functionality or performance

View File

@ -1,60 +1,94 @@
use clap::{Parser, ValueEnum};
use std::path::PathBuf;
/// Color output control options
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum Color {
/// Automatically detect if output supports color
Auto,
/// Always use colored output
Always,
/// Never use colored output
Never,
}
#[derive(Parser, Debug)]
#[command(name = "asn1parser", about = "DER ASN.1 pretty-printer using yasna")]
#[command(
name = "asn1parse",
version,
about = "DER ASN.1 pretty-printer using yasna",
long_about = "A command-line tool for parsing and pretty-printing DER-encoded ASN.1 data. Supports reading from files, stdin, or hex strings with automatic format detection."
)]
pub struct CliArgs {
#[arg(long, value_enum, default_value_t = Color::Auto)]
pub color: Color,
/// Hex-encoded DER input (read from stdin if omitted)
#[arg(help = "Hex-encoded DER input (read from stdin if omitted)")]
pub hex: Option<String>,
/// Color output control
#[arg(long, value_enum, default_value_t = Color::Auto, help = "Control colored output")]
pub color: Color,
/// Read input from file (mutually exclusive with HEX argument)
#[arg(short, long)]
#[arg(short, long, help = "Read input from file (mutually exclusive with HEX argument)")]
pub file: Option<PathBuf>,
/// Input format selection for file/stdin
#[arg(long, value_enum, default_value_t = InputFormat::Auto)]
#[arg(
long,
value_enum,
default_value_t = InputFormat::Auto,
help = "Input format selection for file/stdin"
)]
pub input_format: InputFormat,
/// Try to decode OCTET/BitString payloads if they contain DER
#[arg(long)]
#[arg(long, help = "Try to decode OCTET/BitString payloads if they contain DER")]
pub recursive: bool,
/// Truncate displayed BIT STRINGs to at most N bits (omit to show all)
#[arg(long, value_name = "N")]
#[arg(
long,
value_name = "N",
help = "Truncate displayed BIT STRINGs to at most N bits (omit to show all)"
)]
pub bits_truncate: Option<usize>,
/// Truncate displayed BIT STRINGs to at most N bytes (hex mode)
#[arg(long, value_name = "N")]
#[arg(
long,
value_name = "N",
help = "Truncate displayed BIT STRINGs to at most N bytes (hex mode)"
)]
pub bits_truncate_bytes: Option<usize>,
/// BIT STRING display format
#[arg(long, value_enum, default_value_t = BitsFormat::Auto)]
#[arg(
long,
value_enum,
default_value_t = BitsFormat::Auto,
help = "BIT STRING display format"
)]
pub bits_format: BitsFormat,
}
/// Input format detection and specification
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum InputFormat {
/// Automatically detect input format (hex text vs binary DER)
Auto,
/// Force interpretation as hex-encoded text
Hex,
/// Force interpretation as binary DER data
Der,
}
// Removed certificate-specific view; generic pretty-printer is always used.
/// BIT STRING display format options
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum BitsFormat {
/// Automatically choose the best format
Auto,
/// Display as binary bits (0s and 1s)
Bits,
/// Display as hexadecimal
Hex,
}

View File

@ -66,19 +66,58 @@ fn main() {
parse_hex_or_exit(&hex)
} else {
// Read from stdin
let mut buf = String::new();
if io::stdin().read_to_string(&mut buf).is_ok() && !buf.trim().is_empty() {
let text = buf;
let candidate = strip_ws(&text);
if matches!(args.input_format, cli::InputFormat::Der) {
// If explicitly der, read raw bytes from stdin (not supported via Read::read_to_string)
eprintln!("Reading raw DER from stdin is not supported in text mode. Use --file or provide hex on stdin.");
std::process::exit(2);
match args.input_format {
cli::InputFormat::Der => {
// Read raw binary data from stdin
let mut buf = Vec::new();
if let Err(e) = io::stdin().read_to_end(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
std::process::exit(2);
}
if buf.is_empty() {
eprintln!("No input provided. Pass HEX, use --file, or pipe data.");
std::process::exit(2);
}
buf
}
cli::InputFormat::Hex => {
// Read text and parse as hex
let mut buf = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
std::process::exit(2);
}
if buf.trim().is_empty() {
eprintln!("No input provided. Pass HEX, use --file, or pipe data.");
std::process::exit(2);
}
let hex = strip_ws(&buf);
parse_hex_or_exit(&hex)
}
cli::InputFormat::Auto => {
// Try to read as text first (for hex), fall back to binary
let mut buf = Vec::new();
if let Err(e) = io::stdin().read_to_end(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
std::process::exit(2);
}
if buf.is_empty() {
eprintln!("No input provided. Pass HEX, use --file, or pipe data.");
std::process::exit(2);
}
// Try to interpret as UTF-8 hex text
if let Ok(text) = String::from_utf8(buf.clone()) {
let candidate = strip_ws(&text);
if candidate.chars().all(|c| c.is_ascii_hexdigit()) && candidate.len() % 2 == 0 && !candidate.is_empty() {
parse_hex_or_exit(&candidate)
} else {
buf // Use as raw binary data
}
} else {
buf // Use as raw binary data
}
}
parse_hex_or_exit(&candidate)
} else {
eprintln!("No input provided. Pass HEX, use --file, or pipe data.");
std::process::exit(2);
}
};