# Red Architecture **Rust implementation of GNU sed - Stream Editor** --- ## Table of Contents 1. [Overview](#overview) 2. [Core Architecture](#core-architecture) 3. [Module Structure](#module-structure) 4. [Key Components](#key-components) 5. [Data Flow](#data-flow) 6. [Bytes and Encoding](#bytes-and-encoding) 7. [Regex Engine](#regex-engine) 8. [GNU sed Compatibility](#gnu-sed-compatibility) 9. [Development Guide](#development-guide) --- ## Overview Red is a drop-in replacement for GNU sed, written in Rust. ### Design Principles 1. **Two-Phase Processing**: Compile scripts once, execute many times 2. **Unified Context**: Single configuration object passed through all modules 3. **Type Safety**: Leverage Rust's type system for correctness 4. **GNU Compatibility**: Match GNU sed behavior --- ## Core Architecture ### Comparison with GNU sed | Aspect | GNU sed | Red | Notes | |--------|---------|-----|-------| | Command representation | 1 struct with union | 2 enums (parser + runtime) | Standard AST→IR pattern | | Processing | Byte-first | String-first (UTF-8) | Raw bytes tracked separately | | Regex engine | POSIX regex + DFA | Custom 3-level matcher | Literal/DFA/NFA | | Memory | Obstack allocator | Standard Rust allocations | | | Jumps | Array indices | Pre-resolved indices | Resolved at compile time | ### High-Level Flow ``` ┌─────────────┐ │ CLI Args │ └──────┬──────┘ │ ▼ ┌─────────────┐ ┌──────────────┐ │ Context │────▶│ Validation │ └──────┬──────┘ └──────────────┘ │ ▼ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Parser │────▶│ Lexer │────▶│ AST Nodes │ └──────┬──────┘ └──────────────┘ └──────────────┘ │ (parser::Command) │ ▼ ┌────────────────────┐ │ convert_to_runtime │ (AST → RuntimeCommand) └──────┬─────────────┘ │ ▼ ┌─────────────┐ │ Commands │ (RuntimeCommand with compiled regexes) └──────┬──────┘ │ ▼ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Engine │────▶│ Evaluator │────▶│ Output │ └─────────────┘ └──────────────┘ └──────────────┘ ``` ### Phase 1: Compilation **Input**: Script text + Context **Output**: List of `RuntimeCommand` **Purpose**: Validate and optimize scripts before execution ```rust pub fn parse_scripts_to_commands( scripts: &[(String, ScriptSource)], ctx: &Context, ) -> Result> ``` ### Phase 2: Execution **Input**: Commands + Input lines **Output**: Transformed text **Purpose**: Apply commands to input data ```rust pub fn execute_over_lines( commands: &[RuntimeCommand], // ... parameters ) -> Result<()> ``` --- ## Module Structure ### Module Dependency Graph ``` main.rs │ ▼ cli.rs ──────────────────────────────────────┐ │ │ ▼ ▼ lib.rs ◄──────────────────────────────── errors.rs │ ├──────────────┬──────────────┬──────────────┐ ▼ ▼ ▼ ▼ parser/ engine/ fileio/ util/ ├── mod.rs ├── mod.rs ├── mod.rs ├── regex.rs ├── lexer.rs ├── exec.rs ├── lines.rs └── version.rs └── ast.rs ├── addr.rs ├── inplace.rs ├── types.rs └── encoding.rs └── pattern_space.rs │ ▼ regex/ ├── mod.rs ├── parser.rs ├── dfa.rs ├── nfa.rs ├── backtrack.rs └── literal.rs ``` ### 1. CLI Layer (`src/cli.rs`, `src/main.rs`) **Responsibility**: Argument parsing and validation - Uses `lexopt` for natural order preservation - Separate `print_usage()` and `help_text()` functions - Exit codes match GNU sed (4 for usage errors) **Key Functions**: ```rust pub fn parse_args() -> Result ``` ### 2. Context Layer (`src/context.rs`) **Responsibility**: Unified configuration management ```rust pub struct Context { // Regex mode pub extended_regex: bool, // POSIX compliance pub posix: PosixMode, pub strict_posix: bool, // Execution modes pub quiet: bool, pub null_data: bool, pub unbuffered: bool, pub separate_files: bool, // In-place editing pub in_place_suffix: Option, pub follow_symlinks: bool, // Formatting pub line_length: usize, // Security pub sandbox: bool, // Scripts (for #n quiet mode detection) pub scripts_with_sources: Vec<(String, ScriptSource)>, } ``` **POSIX Modes**: ```rust pub enum PosixMode { Extended, // GNU extensions enabled (default) Correct, // POSIXLY_CORRECT env var Basic, // --posix flag (strict) } ``` ### 3. File I/O Layer (`src/fileio/`) **Responsibility**: File reading, encoding detection, and backup utilities - `encoding.rs` - Encoding detection from BOM and locale - `lines.rs` - File/stdin reading with line splitting - `inplace.rs` - Backup suffix expansion for in-place editing - `mod.rs` - Module exports and output flushing helper **Key Functions**: ```rust pub fn read_all_lines(files, null_data, follow_symlinks) -> Result<(lines, bytes, filenames, encoding, ends_with_sep)> pub fn split_file_content(content, null_data) -> (lines, bytes, encoding, ends_with_sep) pub fn expand_backup_suffix(file_path, suffix) -> String pub fn flush_output(out, unbuffered) ``` ### 4. Parser Layer (`src/parser/`) **Responsibility**: Script compilation and validation #### Lexer (`src/parser/lexer.rs`) Tokenizes sed scripts using a state machine approach. **Key optimizations**: - Single-pass tokenization - Bracket depth tracking - Escape sequence handling - Raw bytes preservation for non-UTF-8 #### AST (`src/parser/ast.rs`) Defines parser command structure: ```rust pub enum Command { Substitute { flags, pattern, replacement, ... }, Delete { range, negated }, Print { range, negated, ... }, Group { commands }, // Flattened at compile time Comment(String), // Discarded at compile time Version { version }, // Checked at compile time // ... more command types } ``` #### Parser (`src/parser.rs`) Converts tokens to AST nodes with validation: ```rust pub fn parse(tokens: &[Token], ctx: &Context) -> Result> ``` ### 5. Engine Layer (`src/engine/`) **Responsibility**: Command execution and state management #### Types (`src/engine/types.rs`) Runtime command representation: ```rust pub enum Command { Substitution { pattern: SedRegex, // Compiled regex replacement: ReplacementTemplate, // Parsed replacement global: bool, print: bool, literal_pattern: Option, // Fast-path optimization literal_replacement: Option, // ... more fields }, // ... more runtime commands } ``` #### ExecutionContext (`src/engine/exec.rs`) Maintains execution state: ```rust pub struct ExecutionContext { pub pattern_space: PatternSpace, pub hold_space: String, pub hold_space_raw: Vec, pub line_number: usize, pub is_last_line: bool, // ... state fields } ``` #### AddressEvaluator (`src/engine/addr.rs`) Evaluates address ranges with state tracking: ```rust pub fn evaluate(&mut self, range: Option<&AddressRange>, ctx: &ExecutionContext) -> bool ``` **Features**: - Line number ranges (5,10) - Regex matches (/pattern/) - Step addressing (1~2) - Dollar ($) for last line - Range state machine (INACTIVE → ACTIVE → CLOSED) ### 6. Error Handling (`src/errors.rs`) **Unified error types**: ```rust pub enum SedError { Parse { message, source }, Io { operation, path, error }, Rename { source_path, dest_path, error }, Runtime { message }, Usage { message }, } ``` **GNU sed compatible error messages and exit codes**. --- ## Key Components ### Two-Stage Command Representation Red uses a deliberate two-stage command representation, following the classic AST-to-IR pattern from compilers: #### Parser Command (`parser/ast.rs`) ```rust pub enum Command { Substitution { pattern: String, // Raw regex pattern replacement: String, // Raw replacement string flags: SubstitutionFlags, // Struct with all flags // ... }, Group { commands: Vec }, // Flattened at compile time Version { version: String }, // Compile-time check only Comment(String), // Discarded // ... more variants } ``` **Purpose**: Preserves source structure, enables error reporting with context #### Runtime Command (`engine/types.rs`) ```rust pub enum Command { Substitution { pattern: SedRegex, // Compiled regex replacement: ReplacementTemplate, // Parsed replacement global: bool, // Expanded from flags print: bool, literal_pattern: Option, // Fast-path optimization literal_replacement: Option, // ... fields with optimizations }, // ... more variants (no Group/Version/Comment) } ``` **Purpose**: Optimized for execution, pre-compiled patterns, literal optimizations #### Why Two Enums? 1. **Separation of concerns**: Parser produces semantic representation, engine uses optimized representation 2. **Compile-time processing**: Group commands are flattened, Version is checked, Comments discarded 3. **Optimization fields**: Runtime adds `literal_pattern`, `literal_replacement`, `use_last` that don't exist in source 4. **Type differences**: String → SedRegex, String → ReplacementTemplate, SubstitutionFlags → individual booleans 5. **Common compiler pattern**: Similar to AST → IR transformation in production compilers ### PatternSpace: Dual Representation The `PatternSpace` struct (`src/engine/pattern_space.rs`) maintains synchronized representations: ```rust pub struct PatternSpace { raw: Vec, // Raw bytes (may contain invalid UTF-8) active_start: usize, // Active pointer for D command optimization text_cache: Option, // Cached UTF-8 representation } ``` **Key methods**: - `raw()` - Returns active bytes slice - `text()` - Returns UTF-8 text (from cache) - `set_raw()` - Updates from bytes, rebuilds text cache with lossy conversion - `delete_first_line()` - O(1) using active pointer (GNU sed technique) --- ## Data Flow ### 1. Script Compilation ``` Script Text │ ▼ ┌─────────────┐ │ Lexer │ Tokenize with raw bytes tracking └──────┬──────┘ │ ▼ ┌─────────────┐ │ Parser │ Build AST (parser::Command) └──────┬──────┘ │ ▼ ┌─────────────┐ │ Validator │ Check POSIX rules, labels └──────┬──────┘ │ ▼ ┌─────────────┐ │ Converter │ compile_to_runtime_commands() └──────┬──────┘ │ ▼ RuntimeCommand[] (with compiled regexes) ``` ### 2. Command Execution ``` Input Lines (with raw bytes) │ ▼ ┌──────────────────┐ │ ExecutionContext │◄─── Commands │ - pattern_space │ │ - hold_space │ │ - line_number │ └────────┬─────────┘ │ ▼ ┌──────────────────────┐ │ apply_commands_with_ │ │ context() │ └───────┬──────────────┘ │ ▼ CommandResult (with raw bytes) │ ▼ ┌────────┐ │ Output │ (preserves raw bytes) └────────┘ ``` ### 3. In-Place Editing ``` Original File │ ▼ ┌──────────────┐ │ Read Content │ (as raw bytes) └──────┬───────┘ │ ▼ ┌──────────────┐ │ Process │ (preserve raw bytes through pipeline) └──────┬───────┘ │ ▼ ┌──────────────┐ │ Temp File │ └──────┬───────┘ │ ▼ ┌──────────────┐ │ Rename │ (atomic) └──────────────┘ ``` --- ## Bytes and Encoding ### Design: UTF-8 First with Raw Bytes Tracking Red uses Rust's `String` type (valid UTF-8) as the primary text representation for regex matching, but maintains raw bytes throughout the pipeline for output. ### Raw Bytes Preservation ``` Input (bytes) → PatternSpace.raw → Commands → Output (bytes) ↓ text_cache (lossy) ↓ Regex matching on text ``` **Implementation**: - Hold space raw bytes tracking (`hold_space_raw: Vec`) - `CommandResult::Continue(String, Option>)` - carries raw bytes through engine - `CommandResult::Print(String, Option>)` - outputs raw bytes when available - `LiteralBytes(Vec)` token in replacement templates ### Component Status | Component | Status | Implementation | |-----------|--------|----------------| | Script reading | Bytes | `Vec` + lossy UTF-8 conversion | | Lexer | Bytes-aware | char-to-byte mapping for raw bytes | | Parser | Bytes-aware | `replacement_raw_bytes` field | | Replacement template | Bytes | `LiteralBytes(Vec)` token | | Escape sequences | Raw output | `\xNN`, `\oNNN`, `\dNNN`, `\cX` | | Pattern matching | UTF-8 | Regex operates on text cache | | Data I/O | Bytes | Raw bytes preserved through hold/pattern space | | Substitution pipeline | Bytes | Raw bytes tracked across multiple substitutions | --- ## Regex Engine Red implements a custom regex engine with three different matchers, each optimized for different pattern types. ### Three-Level Optimization Strategy ```rust pub enum Matcher { Literal(LiteralMatcher), // Fastest: direct string/byte search Dfa(DfaMatcher), // Fast: Deterministic Finite Automaton Nfa(NfaMatcher), // Full-featured: Nondeterministic Finite Automaton } ``` ### Matcher Selection (compile time) ``` Pattern Analysis │ ▼ ┌─────────────────────┐ │ Is literal? │──Yes──▶ LiteralMatcher (fastest) │ (no metacharacters) │ └────────┬────────────┘ │ No ▼ ┌─────────────────┐ │ Has backrefs? │──Yes──▶ NfaMatcher (required) │ (\1, \2, etc) │ └────────┬────────┘ │ No ▼ ┌─────────────────┐ │ MBCS locale? │──Yes──▶ NfaMatcher (for non-ASCII patterns) │ (MB_CUR_MAX > 1)│ └────────┬────────┘ │ No ▼ DfaMatcher (fast, no backrefs) ``` ### Literal Matcher (`src/regex/literal.rs`) **When used**: Patterns without any regex metacharacters (e.g., `hello`, `foo_bar`) **Implementation**: ```rust pub struct LiteralMatcher { pattern: String, ignore_case: bool, } impl LiteralMatcher { // String-based matching (UTF-8) pub fn find(&self, text: &str) -> Option // Byte-based matching (MBCS-safe for ASCII patterns) pub fn find_bytes(&self, bytes: &[u8]) -> Option pub fn find_bytes_from(&self, bytes: &[u8], start: usize) -> Option } ``` **MBCS behavior**: ASCII-only literal patterns can safely use byte-level matching even in MBCS locales, because ASCII bytes (0x00-0x7F) never appear as part of multibyte characters. ### DFA Matcher (`src/regex/dfa.rs`) **When used**: Simple patterns without backreferences in single-byte locales **How DFA works**: - Compiles pattern into a state machine at parse time - Each state has exactly one transition per input character - No backtracking - processes input in a single pass - Memory: O(states × alphabet_size) **Limitations**: - **Cannot support backreferences** - This is a mathematical impossibility, not an implementation limitation. Backreferences (`\1`, `\2`) require the regex engine to "remember" what was matched and compare against it later. DFAs have no memory of the path taken to reach a state. - **Disabled in MBCS locales** - DFA operates on fixed-width input units. In MBCS, character widths vary (1-4 bytes), requiring boundary detection that DFA architecture doesn't support. ### NFA Matcher (`src/regex/nfa.rs`, `src/regex/backtrack.rs`) **When used**: - Patterns with backreferences (`\1`, `\2`, etc.) - Patterns with capturing groups that need extraction - All patterns in MBCS locales (for non-ASCII patterns) **How NFA differs from DFA**: ``` DFA NFA ┌─────────────────┐ ┌─────────────────┐ Input 'a' → │ State 1 ──────▶ │ State 2 │ State 1 ──┬───▶ │ State 2 │ (one path) │ │ └───▶ │ State 3 └─────────────────┘ │ (multiple) │ └─────────────────┘ ``` - NFA can be in multiple states simultaneously - Explores all possible paths (via backtracking) - Can "remember" captured groups for backreferences - More flexible but slower **MBCS support in NFA**: ```rust // NFA uses MbText for character boundary tracking pub struct MbText<'a> { raw: &'a [u8], char_boundaries: Vec, // Where each character starts char_validity: Vec, // Is each sequence valid? } // Transitions respect character boundaries match transition { Transition::Any => { // In MBCS mode: advance by full character, not byte let char_len = mbtext.char_at(pos).len(); next_pos = pos + char_len; } } ``` ### Why Both DFA and NFA? | Aspect | DFA | NFA | |--------|-----|-----| | Speed | O(n) - very fast | O(n×m) - slower | | Backreferences | Impossible | Full support | | Memory usage | Higher (state table) | Lower | | MBCS support | Disabled | Full support | | Use case | Simple patterns | Complex patterns | **Cannot remove either**: - Removing DFA: Performance regression for simple patterns - Removing NFA: Backreferences would be impossible (`s/\(foo\)/\1\1/` fails) ### MBCS Locale Behavior When `MB_CUR_MAX > 1` (Shift-JIS, EUC-JP, etc.): ``` Pattern Type Matcher Used Why ───────────────────────────────────────────────────── ASCII literal "hello" LiteralMatcher Safe: ASCII bytes don't overlap MBCS Non-ASCII literal "日本" NfaMatcher Needs MBCS boundary handling Any with backrefs NfaMatcher DFA can't do backrefs Simple /[a-z]+/ NfaMatcher DFA disabled in MBCS ``` **Example of why DFA fails in MBCS**: ``` Shift-JIS "表" = bytes [0x95, 0x5C] ASCII "\" = byte [0x5C] Pattern: /\\/ (match backslash) DFA (byte-level): Would incorrectly match inside "表" because 0x5C appears as second byte NFA (MBCS-aware): Correctly skips "表" because MbText knows 0x5C is part of a 2-byte character ``` --- ## GNU sed Compatibility ### Compatibility Features #### CLI Compatibility - Natural order preservation of `-e` and `-f` - Separate usage (stderr) vs help (stdout) messages - Exit code 4 for usage errors - Write error detection (`/dev/full`) #### Command Compatibility - All standard sed commands (s, d, p, a, i, c, etc.) - Address ranges (1,10, /start/,/end/, $) - Step addressing (1~2) - Hold space operations (h, H, g, G, x) - Branching (b, t, T, :label) - Byte-level operations (l command with octal escapes) #### Multibyte Character Set (MBCS) Support Full MBCS support for non-UTF-8 locales (Shift-JIS, EUC-JP, etc.) | Feature | Implementation | |---------|----------------| | Locale detection | `mbcs::initialize()` via `setlocale()`, `MB_CUR_MAX` | | Character boundaries | `MbText` struct with `mbrlen()` | | Regex dot matching | `Transition::Any` respects MB boundaries | | Character classes | `mbrtowc()` conversion for CharSet matching | | Capture groups | `raw_bytes` field preserves MBCS bytes | | Backreferences | Byte-level comparison in MB mode | | Invalid/incomplete sequences | `char_validity` tracking, skip in dot match | | Raw bytes output | `render_replacement_to_bytes()` sink | **Key MBCS Data Structures**: ```rust pub struct MbText<'a> { raw: &'a [u8], // Raw input bytes char_boundaries: Vec, // Byte offset for each char start char_validity: Vec, // Valid vs invalid/incomplete } pub struct MbChar<'a> { bytes: &'a [u8], // One logical character's bytes } ``` #### SELinux Support When built with the `selinux` feature (Linux only): - Preserves SELinux contexts during in-place editing - Without `--follow-symlinks`: Uses symlink's context for new file - With `--follow-symlinks`: Uses target file's context --- ## Development Guide ### Adding New Commands 1. Add token in `src/parser/lexer.rs` 2. Add AST node in `src/parser/ast.rs` 3. Add parsing logic in `src/parser.rs` 4. Add runtime command in `src/engine/types.rs` 5. Add conversion in `src/lib.rs:convert_new_command_to_old()` 6. Add execution logic in `src/engine/exec.rs` 7. Add tests in `tests/` ### Adding New Options 1. Add field to `CliArgs` in `src/cli.rs` 2. Add parsing in `parse_args()` 3. Add to `Context` in `src/context.rs` 4. Update `RunConfig` in `src/lib.rs` 5. Add validation if needed ### Testing ```bash # Unit tests cargo test # GNU sed compatibility tests ./scripts/run_gnused_tests.sh # Specific test ./scripts/run_gnused_tests.sh --test-pattern "help.sh" # Fast mode (debug build, compact output) ./scripts/run_gnused_tests.sh --fast ``` --- ## References - [GNU sed Manual](https://www.gnu.org/software/sed/manual/) - [POSIX sed Specification](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html)