Files
red/ARCHITECTURE.md
Volodymyr Yavdoshenko be09c6a4a5 red v1.0.0
2026-01-08 22:52:12 +02:00

778 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<Vec<RuntimeCommand>>
```
### 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<CliArgs>
```
### 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<String>,
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<Vec<Command>>
```
### 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<String>, // Fast-path optimization
literal_replacement: Option<String>,
// ... 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<u8>,
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<Command> }, // 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<String>, // Fast-path optimization
literal_replacement: Option<String>,
// ... 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<u8>, // Raw bytes (may contain invalid UTF-8)
active_start: usize, // Active pointer for D command optimization
text_cache: Option<String>, // 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<u8>`)
- `CommandResult::Continue(String, Option<Vec<u8>>)` - carries raw bytes through engine
- `CommandResult::Print(String, Option<Vec<u8>>)` - outputs raw bytes when available
- `LiteralBytes(Vec<u8>)` token in replacement templates
### Component Status
| Component | Status | Implementation |
|-----------|--------|----------------|
| Script reading | Bytes | `Vec<u8>` + 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<u8>)` 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<usize>
// Byte-based matching (MBCS-safe for ASCII patterns)
pub fn find_bytes(&self, bytes: &[u8]) -> Option<usize>
pub fn find_bytes_from(&self, bytes: &[u8], start: usize) -> Option<usize>
}
```
**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<usize>, // Where each character starts
char_validity: Vec<bool>, // 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<usize>, // Byte offset for each char start
char_validity: Vec<bool>, // 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)