Skip to main content

zsh/extensions/
zwc.rs

1//! ZWC (Zsh Word Code) file parser. Direct port of the dump-file
2//! family in zsh/Src/parse.c:3077-end.
3//!
4//! Parses compiled zsh function files (.zwc) into function definitions
5//! that can be executed by zshrs. Counterpart to zsh's bin_zcompile
6//! (parse.c:3179-3257) for the build side and try_dump_file /
7//! try_source_file / check_dump_file (parse.c:3746-3833) for the
8//! load side.
9//!
10//! Format constants (FD_MAGIC, FD_OMAGIC, FD_PRELEN, FDF_MAP,
11//! FDF_OTHER, FDHF_KSHLOAD, FDHF_ZSHLOAD) match parse.c:3104-3151
12//! exactly so .zwc files written by stock zsh load in zshrs and
13//! vice versa.
14//!
15//! Direct port surface mapping:
16//!
17//!   ZwcFile::load            <- parse.c:3746-3793 try_dump_file
18//!   ZwcFile::get_function    <- parse.c:3166-3176 dump_find_func
19//!   ZwcFile::list_functions  <- parse.c:fdheaderlen + nextfdhead walk
20//!   ZwcFile::decode_function <- parse.c:3245-3543 dump_func / build
21//!   ZwcBuilder::new          <- parse.c:3179-3257 bin_zcompile init
22//!   ZwcBuilder::add_source   <- parse.c:3397-3535 build_dump body
23//!   ZwcBuilder::add_file     <- parse.c:3536-3631 build_cur_dump
24//!   ZwcBuilder::write        <- parse.c:bld_eprog + dump-write loop
25//!   WordcodeDecoder          <- parse.c:wc_code/wc_data helpers
26//!   wc_code / wc_data        <- parse.c:wc_code/wc_data macros
27
28use crate::parse::{
29    CaseTerminator, CompoundCommand, ListOp, Redirect, RedirectOp, ShellCommand, ShellWord,
30    SimpleCommand,
31};
32use std::fs::File;
33use std::io::Write;
34use std::io::{self, Read, Seek, SeekFrom};
35use std::path::Path;
36
37const FD_MAGIC: u32 = 0x04050607;
38const FD_OMAGIC: u32 = 0x07060504; // Other byte order
39const FD_PRELEN: usize = 12;
40use crate::ported::zsh_h::{
41    Bang, Bar, Bnull, Comma, Dash, Dnull, Equals, Hat, Inang, Inbrace, Inbrack, Inpar, Nularg,
42    Outang, Outbrace, Outbrack, Outpar, Pound, Quest, Snull, Star, Stringg, Tick, Tilde, WC_ARITH,
43    WC_ASSIGN, WC_ASSIGN_ARRAY, WC_ASSIGN_INC, WC_ASSIGN_SCALAR, WC_AUTOFN, WC_CASE, WC_CASE_AND,
44    WC_CASE_FREE, WC_CASE_HEAD, WC_CASE_OR, WC_CASE_TESTAND, WC_CODEBITS, WC_COND, WC_CURSH,
45    WC_END, WC_FOR, WC_FOR_COND, WC_FOR_LIST, WC_FOR_PPARAM, WC_FUNCDEF, WC_IF, WC_IF_ELIF,
46    WC_IF_ELSE, WC_IF_HEAD, WC_IF_IF, WC_LIST, WC_LIST_FREE, WC_PIPE, WC_PIPE_END, WC_PIPE_MID,
47    WC_REDIR, WC_REPEAT, WC_SELECT, WC_SELECT_LIST, WC_SELECT_PPARAM, WC_SIMPLE, WC_SUBLIST,
48    WC_SUBLIST_AND, WC_SUBLIST_COPROC, WC_SUBLIST_END, WC_SUBLIST_FREE, WC_SUBLIST_NOT,
49    WC_SUBLIST_OR, WC_SUBLIST_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_TYPESET, WC_WHILE,
50    WC_WHILE_UNTIL, WC_WHILE_WHILE,
51};
52// Z_END / Z_SIMPLE in zsh_h are i32 (matching C `int` for these flag bits).
53// Rebind to u32 for bitwise ops against `wordcode` data.
54const Z_END: u32 = crate::ported::zsh_h::Z_END as u32;
55const Z_SIMPLE: u32 = crate::ported::zsh_h::Z_SIMPLE as u32;
56
57/// Decode one NUL-delimited entry from a C-convention wordcode
58/// string pool into the Rust-internal token-char `String` form.
59///
60/// Bridge for `ecgetstr` / `ecrawstr` (`Src/parse.c:2855/2891` ports):
61/// C treats `prog->strs` as raw `char *` — token codes (Pound `0x84`
62/// .. Nularg `0xa1`), Meta (`0x83`), and multibyte text all live as
63/// raw bytes. zshrs's internal convention stores tokens as `char`s
64/// (so a native eprog's pool is valid UTF-8 and passes through
65/// unchanged), but an eprog loaded from a C-zsh-written `.zwc`
66/// (`check_dump_file`, parse.c:3833 port) carries the raw single
67/// bytes. Widening each non-UTF-8 byte to the char of the same
68/// codepoint yields exactly the token representation
69/// `lex::untokenize` and the rest of the pipeline expect, while
70/// genuine UTF-8 text (e.g. literal `é`) survives intact.
71pub fn wordcode_pool_str(bytes: &[u8]) -> String {
72    let mut out = String::with_capacity(bytes.len());
73    let mut rest = bytes;
74    loop {
75        match std::str::from_utf8(rest) {
76            Ok(s) => {
77                out.push_str(s);
78                break;
79            }
80            Err(e) => {
81                let (valid, after) = rest.split_at(e.valid_up_to());
82                // SAFETY: `valid_up_to` guarantees this prefix is UTF-8.
83                out.push_str(unsafe { std::str::from_utf8_unchecked(valid) });
84                out.push(after[0] as char);
85                rest = &after[1..];
86            }
87        }
88    }
89    out
90}
91
92/// Untokenize a zsh tokenized string back to shell syntax
93pub(crate) fn untokenize(bytes: &[u8]) -> String {
94    let mut result = String::new();
95    let mut i = 0;
96
97    while i < bytes.len() {
98        let b = bytes[i];
99        // Token constants in zsh_h are `char` (Unicode \u{84}..\u{a1}).
100        // Tokenized strings encode them as the same byte value, so widen
101        // the byte to char for the match.
102        let c = b as char;
103        match c {
104            Pound => result.push('#'),
105            Stringg => result.push('$'),
106            Hat => result.push('^'),
107            Star => result.push('*'),
108            Inpar => result.push('('),
109            Outpar => result.push(')'),
110            Equals => result.push('='),
111            Bar => result.push('|'),
112            Inbrace => result.push('{'),
113            Outbrace => result.push('}'),
114            Inbrack => result.push('['),
115            Outbrack => result.push(']'),
116            Tick => result.push('`'),
117            Inang => result.push('<'),
118            Outang => result.push('>'),
119            Quest => result.push('?'),
120            Tilde => result.push('~'),
121            Comma => result.push(','),
122            Dash => result.push('-'),
123            Bang => result.push('!'),
124            Snull | Dnull | Bnull | Nularg => {
125                // Skip null markers
126            }
127            '\u{89}' => result.push_str("(("), // Inparmath
128            '\u{8b}' => result.push_str("))"), // Outparmath
129            _ if b >= 0x80 => {
130                // Unknown token, skip or try to represent
131            }
132            _ => result.push(c),
133        }
134        i += 1;
135    }
136
137    result
138}
139
140/// Extract the opcode portion of a wordcode word.
141/// Port of the `wc_code()` macro from Src/zsh.h.
142#[inline]
143pub fn wc_code(c: u32) -> u32 {
144    c & ((1 << WC_CODEBITS) - 1)
145}
146
147/// Extract the data portion of a wordcode word.
148/// Port of the `wc_data()` macro from Src/zsh.h.
149#[inline]
150pub fn wc_data(c: u32) -> u32 {
151    c >> WC_CODEBITS
152}
153
154/// `.zwc` file header.
155/// Port of `struct fdhead` from Src/parse.c — the C source's
156/// `bld_eprog()` (line 547) writes this layout when persisting a
157/// compiled function to a `.zwc` cache file.
158#[derive(Debug)]
159pub struct ZwcHeader {
160    /// `magic` field.
161    pub magic: u32,
162    /// `flags` field.
163    pub flags: u8,
164    /// `version` field.
165    pub version: String,
166    /// `header_len` field.
167    pub header_len: u32,
168    /// `other_offset` field.
169    pub other_offset: u32,
170}
171
172/// One function's directory entry inside a `.zwc` file.
173/// Port of `struct fdname` from Src/parse.c — `bld_eprog()`
174/// (line 547) emits one entry per function autoloaded from the
175/// cache.
176#[derive(Debug)]
177pub struct ZwcFunction {
178    /// `name` field.
179    pub name: String,
180    /// `start` field.
181    pub start: u32,
182    /// `len` field.
183    pub len: u32,
184    /// `npats` field.
185    pub npats: u32,
186    /// `strs_offset` field.
187    pub strs_offset: u32,
188    /// `flags` field.
189    pub flags: u32,
190}
191
192/// A loaded `.zwc` file.
193/// Aggregates the header / function-table / wordcode / strings the
194/// C source's `try_source_file()` (Src/init.c) reads from a `.zwc`
195/// before caching the parsed eprog.
196#[derive(Debug)]
197pub struct ZwcFile {
198    /// `header` field.
199    pub header: ZwcHeader,
200    /// `functions` field.
201    pub functions: Vec<ZwcFunction>,
202    /// `wordcode` field.
203    pub wordcode: Vec<u32>,
204    /// `strings` field.
205    pub strings: Vec<u8>,
206}
207
208impl ZwcFile {
209    /// `load` — see implementation.
210    pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
211        let mut file = File::open(path)?;
212        let mut buf = vec![0u8; (FD_PRELEN + 1) * 4];
213
214        file.read_exact(&mut buf)?;
215
216        let magic = u32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
217
218        let swap_bytes = if magic == FD_MAGIC {
219            false
220        } else if magic == FD_OMAGIC {
221            true
222        } else {
223            return Err(io::Error::new(
224                io::ErrorKind::InvalidData,
225                format!("Invalid ZWC magic: 0x{:08x}", magic),
226            ));
227        };
228
229        let read_u32 = |bytes: &[u8], offset: usize| -> u32 {
230            let b = &bytes[offset..offset + 4];
231            let val = u32::from_ne_bytes([b[0], b[1], b[2], b[3]]);
232            if swap_bytes {
233                val.swap_bytes()
234            } else {
235                val
236            }
237        };
238
239        let flags = buf[4];
240        let other_offset = (buf[5] as u32) | ((buf[6] as u32) << 8) | ((buf[7] as u32) << 16);
241
242        // Version string starts at offset 8 (word 2)
243        let version_start = 8;
244        let version_end = buf[version_start..]
245            .iter()
246            .position(|&b| b == 0)
247            .map(|p| version_start + p)
248            .unwrap_or(buf.len());
249        let version = String::from_utf8_lossy(&buf[version_start..version_end]).to_string();
250
251        let header_len = read_u32(&buf, FD_PRELEN * 4);
252
253        let header = ZwcHeader {
254            magic,
255            flags,
256            version,
257            header_len,
258            other_offset,
259        };
260
261        // Read full header
262        file.seek(SeekFrom::Start(0))?;
263        let full_header_size = (header_len as usize) * 4;
264        let mut header_buf = vec![0u8; full_header_size];
265        file.read_exact(&mut header_buf)?;
266
267        // Parse function headers (start after FD_PRELEN words)
268        let mut functions = Vec::new();
269        let mut offset = FD_PRELEN * 4;
270
271        while offset < full_header_size {
272            if offset + 24 > full_header_size {
273                break;
274            }
275
276            let start = read_u32(&header_buf, offset);
277            let len = read_u32(&header_buf, offset + 4);
278            let npats = read_u32(&header_buf, offset + 8);
279            let strs = read_u32(&header_buf, offset + 12);
280            let hlen = read_u32(&header_buf, offset + 16);
281            let flags = read_u32(&header_buf, offset + 20);
282
283            // Name follows the header struct (6 words = 24 bytes)
284            let name_start = offset + 24;
285            let name_end = header_buf[name_start..]
286                .iter()
287                .position(|&b| b == 0)
288                .map(|p| name_start + p)
289                .unwrap_or(full_header_size);
290
291            let name = String::from_utf8_lossy(&header_buf[name_start..name_end]).to_string();
292
293            if name.is_empty() {
294                break;
295            }
296
297            functions.push(ZwcFunction {
298                name,
299                start,
300                len,
301                npats,
302                strs_offset: strs,
303                flags,
304            });
305
306            // Move to next function header
307            offset += (hlen as usize) * 4;
308        }
309
310        // Read the rest of the file (wordcode + strings)
311        let mut rest = Vec::new();
312        file.read_to_end(&mut rest)?;
313
314        // Parse wordcode as u32 array
315        let mut wordcode = Vec::new();
316        let mut i = 0;
317        while i + 4 <= rest.len() {
318            let val = u32::from_ne_bytes([rest[i], rest[i + 1], rest[i + 2], rest[i + 3]]);
319            wordcode.push(if swap_bytes { val.swap_bytes() } else { val });
320            i += 4;
321        }
322
323        // Strings are embedded after wordcode for each function
324        let strings = rest;
325
326        Ok(ZwcFile {
327            header,
328            functions,
329            wordcode,
330            strings,
331        })
332    }
333    /// `list_functions` — see implementation.
334    pub fn list_functions(&self) -> Vec<&str> {
335        self.functions.iter().map(|f| f.name.as_str()).collect()
336    }
337    /// `function_count` — see implementation.
338    pub fn function_count(&self) -> usize {
339        self.functions.len()
340    }
341
342    /// Create a new empty ZWC file for building
343    pub fn new_builder() -> ZwcBuilder {
344        ZwcBuilder::new()
345    }
346    /// `get_function` — see implementation.
347    pub fn get_function(&self, name: &str) -> Option<&ZwcFunction> {
348        self.functions
349            .iter()
350            .find(|f| f.name == name || f.name.ends_with(&format!("/{}", name)))
351    }
352    /// `decode_function` — see implementation.
353    pub fn decode_function(&self, func: &ZwcFunction) -> Option<DecodedFunction> {
354        let header_words = self.header.header_len as usize;
355        let start_idx = (func.start as usize).saturating_sub(header_words);
356
357        if start_idx >= self.wordcode.len() {
358            return None;
359        }
360
361        // Strings are embedded at strs_offset bytes from the start of this function's wordcode
362        // Convert byte offset to word offset to find where strings start
363        let func_wordcode = &self.wordcode[start_idx..];
364
365        // The strings are at byte offset strs_offset from the wordcode base
366        // Create a string table from the wordcode bytes
367        let mut string_bytes = Vec::new();
368        for &wc in func_wordcode {
369            string_bytes.extend_from_slice(&wc.to_ne_bytes());
370        }
371
372        let decoder = WordcodeDecoder::new(func_wordcode, &string_bytes, func.strs_offset as usize);
373
374        Some(DecodedFunction {
375            name: func.name.clone(),
376            body: decoder.decode(),
377        })
378    }
379}
380
381/// Builder for emitting `.zwc` files.
382/// Port of `bld_eprog(int heap)` from Src/parse.c:547 — accumulates
383/// function source / wordcode / strings, then writes them out in
384/// the canonical layout `try_source_file()` (Src/init.c) reads.
385#[derive(Debug)]
386pub struct ZwcBuilder {
387    functions: Vec<(String, Vec<u8>)>, // (name, source code)
388}
389
390impl Default for ZwcBuilder {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396impl ZwcBuilder {
397    /// `new` — see implementation.
398    pub fn new() -> Self {
399        Self {
400            functions: Vec::new(),
401        }
402    }
403
404    /// Add a function from source code
405    pub fn add_source(&mut self, name: &str, source: &str) {
406        self.functions
407            .push((name.to_string(), source.as_bytes().to_vec()));
408    }
409
410    /// Add a function from a file
411    pub fn add_file(&mut self, path: &std::path::Path) -> io::Result<()> {
412        let name = path
413            .file_name()
414            .and_then(|n| n.to_str())
415            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid filename"))?;
416        let source = std::fs::read(path)?;
417        self.functions.push((name.to_string(), source));
418        Ok(())
419    }
420
421    /// Write the ZWC file
422    /// Note: This writes a simplified format that stores raw source code
423    /// rather than compiled wordcode. The loader handles both formats.
424    pub fn write<P: AsRef<std::path::Path>>(&self, path: P) -> io::Result<()> {
425        let mut file = std::fs::File::create(path)?;
426
427        // Write magic
428        file.write_all(&FD_MAGIC.to_ne_bytes())?;
429
430        // Write flags (0 = not mapped)
431        file.write_all(&[0u8])?;
432
433        // Write other offset placeholder (3 bytes)
434        file.write_all(&[0u8; 3])?;
435
436        // Write version string (padded to 4-byte boundary)
437        let version = env!("CARGO_PKG_VERSION");
438        let version_bytes = version.as_bytes();
439        file.write_all(version_bytes)?;
440        file.write_all(&[0u8])?; // null terminator
441                                 // Pad to 4-byte boundary
442        let padding = (4 - ((version_bytes.len() + 1) % 4)) % 4;
443        file.write_all(&vec![0u8; padding])?;
444
445        // Calculate header length (in words)
446        let mut header_words = FD_PRELEN;
447        for (name, _) in &self.functions {
448            // 6 words for fdhead struct + name (padded)
449            header_words += 6 + (name.len() + 1).div_ceil(4);
450        }
451
452        // Write header length
453        file.write_all(&(header_words as u32).to_ne_bytes())?;
454
455        // Track positions for function data
456        let mut data_offset = header_words;
457        let mut func_data: Vec<(u32, u32, Vec<u8>)> = Vec::new(); // (start, len, data)
458
459        // Write function headers
460        for (name, source) in &self.functions {
461            let source_words = source.len().div_ceil(4);
462
463            // fdhead: start, len, npats, strs, hlen, flags
464            file.write_all(&(data_offset as u32).to_ne_bytes())?; // start
465            file.write_all(&(source.len() as u32).to_ne_bytes())?; // len (in bytes)
466            file.write_all(&0u32.to_ne_bytes())?; // npats
467            file.write_all(&0u32.to_ne_bytes())?; // strs offset
468            let hlen = 6 + (name.len() + 1).div_ceil(4);
469            file.write_all(&(hlen as u32).to_ne_bytes())?; // hlen
470            file.write_all(&0u32.to_ne_bytes())?; // flags
471
472            // Write name (null-terminated, padded)
473            file.write_all(name.as_bytes())?;
474            file.write_all(&[0u8])?;
475            let name_padding = (4 - ((name.len() + 1) % 4)) % 4;
476            file.write_all(&vec![0u8; name_padding])?;
477
478            func_data.push((data_offset as u32, source.len() as u32, source.clone()));
479            data_offset += source_words;
480        }
481
482        // Write function data (source code, padded to 4 bytes)
483        for (_, _, data) in &func_data {
484            file.write_all(data)?;
485            let padding = (4 - (data.len() % 4)) % 4;
486            file.write_all(&vec![0u8; padding])?;
487        }
488
489        Ok(())
490    }
491}
492
493/// Decoded function body (one entry per `.zwc` directory entry).
494/// zshrs convenience for tooling (`zshrs zwc dump`); C zsh just
495/// re-eprogs from the wordcode (Src/parse.c) without exposing the
496/// per-op tree.
497#[derive(Debug, Clone)]
498pub struct DecodedFunction {
499    /// `name` field.
500    pub name: String,
501    /// `body` field.
502    pub body: Vec<DecodedOp>,
503}
504
505/// Decoded wordcode op variants.
506/// Mirrors the `WC_*` opcode dispatch tree the C source uses
507/// across Src/exec.c (`exectree()`) and Src/text.c
508/// (`gettext2()`). Each variant corresponds to one of the C
509/// source's `WC_*` opcodes.
510#[derive(Debug, Clone)]
511pub enum DecodedOp {
512    /// `End` variant.
513    End,
514    /// `LineNo` variant.
515    LineNo(u32),
516    /// `List` variant.
517    List {
518        list_type: u32,
519        is_end: bool,
520        ops: Vec<DecodedOp>,
521    },
522    /// `Sublist` variant.
523    Sublist {
524        sublist_type: u32,
525        negated: bool,
526        ops: Vec<DecodedOp>,
527    },
528    /// `Pipe` variant.
529    Pipe { lineno: u32, ops: Vec<DecodedOp> },
530    /// `Redir` variant.
531    Redir {
532        redir_type: u32,
533        fd: i32,
534        target: String,
535        varid: Option<String>,
536    },
537    /// `Assign` variant.
538    Assign { name: String, value: String },
539    /// `AssignArray` variant.
540    AssignArray { name: String, values: Vec<String> },
541    /// `Simple` variant.
542    Simple { args: Vec<String> },
543    /// `Typeset` variant.
544    Typeset {
545        args: Vec<String>,
546        assigns: Vec<DecodedOp>,
547    },
548    /// `Subsh` variant.
549    Subsh { ops: Vec<DecodedOp> },
550    /// `Cursh` variant.
551    Cursh { ops: Vec<DecodedOp> },
552    /// `Timed` variant.
553    Timed { cmd: Option<Box<DecodedOp>> },
554    /// `FuncDef` variant.
555    FuncDef { name: String, body: Vec<DecodedOp> },
556    /// `For` variant.
557    For {
558        var: String,
559        list: Vec<String>,
560        body: Vec<DecodedOp>,
561    },
562    /// `ForCond` variant.
563    ForCond {
564        init: String,
565        cond: String,
566        step: String,
567        body: Vec<DecodedOp>,
568    },
569    /// `Select` variant.
570    Select {
571        var: String,
572        list: Vec<String>,
573        body: Vec<DecodedOp>,
574    },
575    /// `While` variant.
576    While {
577        cond: Vec<DecodedOp>,
578        body: Vec<DecodedOp>,
579        is_until: bool,
580    },
581    /// `Repeat` variant.
582    Repeat { count: String, body: Vec<DecodedOp> },
583    /// `Case` variant.
584    Case {
585        word: String,
586        cases: Vec<(String, Vec<DecodedOp>)>,
587    },
588    /// `CaseItem` variant.
589    CaseItem {
590        pattern: String,
591        terminator: u32,
592        body: Vec<DecodedOp>,
593    },
594    /// `If` variant.
595    If {
596        if_type: u32,
597        conditions: Vec<(Vec<DecodedOp>, Vec<DecodedOp>)>,
598        else_body: Option<Vec<DecodedOp>>,
599    },
600    /// `Cond` variant.
601    Cond { cond_type: u32, args: Vec<String> },
602    /// `Arith` variant.
603    Arith { expr: String },
604    /// `AutoFn` variant.
605    AutoFn,
606    /// `Try` variant.
607    Try {
608        try_body: Vec<DecodedOp>,
609        always_body: Vec<DecodedOp>,
610    },
611    /// `Unknown` variant.
612    Unknown { code: u32, data: u32 },
613}
614
615/// Wordcode-byte cursor for decoding `.zwc` blobs.
616/// Inverse of the C source's `bld_eprog()` (Src/parse.c:547) —
617/// walks the same WC_* dispatch tree as `gettext2()` (Src/text.c)
618/// but emits the typed `DecodedOp` AST instead of source text.
619pub struct WordcodeDecoder<'a> {
620    /// `code` field.
621    code: &'a [u32],
622    /// `strings` field.
623    strings: &'a [u8],
624    /// `strs_base` field.
625    strs_base: usize,
626    /// `pos` field.
627    pub pos: usize,
628}
629
630impl<'a> WordcodeDecoder<'a> {
631    /// `new` — see implementation.
632    pub fn new(code: &'a [u32], strings: &'a [u8], strs_base: usize) -> Self {
633        Self {
634            code,
635            strings,
636            strs_base,
637            pos: 0,
638        }
639    }
640    /// `at_end` — see implementation.
641    pub fn at_end(&self) -> bool {
642        self.pos >= self.code.len()
643    }
644    /// `peek` — see implementation.
645    pub fn peek(&self) -> Option<u32> {
646        self.code.get(self.pos).copied()
647    }
648    /// `next` — see implementation.
649    #[allow(clippy::should_implement_trait)]
650    pub fn next(&mut self) -> Option<u32> {
651        let val = self.code.get(self.pos).copied();
652        if val.is_some() {
653            self.pos += 1;
654        }
655        val
656    }
657    /// `read_string` — see implementation.
658    pub fn read_string(&mut self) -> String {
659        let wc = self.next().unwrap_or(0);
660        self.decode_string(wc)
661    }
662    /// `decode_string` — see implementation.
663    pub fn decode_string(&self, wc: u32) -> String {
664        // Zsh string encoding from ecrawstr():
665        // - c == 6 || c == 7 -> empty string
666        // - c & 2 (bit 1 set) -> short string, chars in bits 3-10, 11-18, 19-26
667        // - otherwise -> long string at strs + (c >> 2)
668
669        if wc == 6 || wc == 7 {
670            return String::new();
671        }
672
673        if (wc & 2) != 0 {
674            // Short string (1-3 chars packed in upper bits)
675            let mut s = String::new();
676            let c1 = ((wc >> 3) & 0xff) as u8;
677            let c2 = ((wc >> 11) & 0xff) as u8;
678            let c3 = ((wc >> 19) & 0xff) as u8;
679            if c1 != 0 {
680                s.push(c1 as char);
681            }
682            if c2 != 0 {
683                s.push(c2 as char);
684            }
685            if c3 != 0 {
686                s.push(c3 as char);
687            }
688            s
689        } else {
690            // Long string (offset into strs from strs_base)
691            let offset = (wc >> 2) as usize;
692            self.get_string_at(self.strs_base + offset)
693        }
694    }
695
696    fn get_string_at(&self, offset: usize) -> String {
697        if offset >= self.strings.len() {
698            return String::new();
699        }
700
701        let end = self.strings[offset..]
702            .iter()
703            .position(|&b| b == 0)
704            .map(|p| offset + p)
705            .unwrap_or(self.strings.len());
706
707        // Untokenize the zsh string - convert tokens back to shell syntax
708        let raw = &self.strings[offset..end];
709        untokenize(raw)
710    }
711
712    /// Decode the wordcode into a list of operations
713    pub fn decode(&self) -> Vec<DecodedOp> {
714        let mut decoder = WordcodeDecoder::new(self.code, self.strings, self.strs_base);
715        decoder.decode_program()
716    }
717
718    fn decode_program(&mut self) -> Vec<DecodedOp> {
719        let mut ops = Vec::new();
720
721        while let Some(wc) = self.peek() {
722            let code = wc_code(wc);
723
724            if code == WC_END {
725                self.next();
726                ops.push(DecodedOp::End);
727                break;
728            }
729
730            if let Some(op) = self.decode_next_op() {
731                ops.push(op);
732            } else {
733                break;
734            }
735        }
736
737        ops
738    }
739
740    fn decode_next_op(&mut self) -> Option<DecodedOp> {
741        let wc = self.next()?;
742        let code = wc_code(wc);
743        let data = wc_data(wc);
744
745        let op = match code {
746            WC_END => DecodedOp::End,
747            WC_LIST => self.decode_list(data),
748            WC_SUBLIST => self.decode_sublist(data),
749            WC_PIPE => self.decode_pipe(data),
750            WC_REDIR => self.decode_redir(data),
751            WC_ASSIGN => self.decode_assign(data),
752            WC_SIMPLE => self.decode_simple(data),
753            WC_TYPESET => self.decode_typeset(data),
754            WC_SUBSH => self.decode_subsh(data),
755            WC_CURSH => self.decode_cursh(data),
756            WC_TIMED => self.decode_timed(data),
757            WC_FUNCDEF => self.decode_funcdef(data),
758            WC_FOR => self.decode_for(data),
759            WC_SELECT => self.decode_select(data),
760            WC_WHILE => self.decode_while(data),
761            WC_REPEAT => self.decode_repeat(data),
762            WC_CASE => self.decode_case(data),
763            WC_IF => self.decode_if(data),
764            WC_COND => self.decode_cond(data),
765            WC_ARITH => self.decode_arith(),
766            WC_AUTOFN => DecodedOp::AutoFn,
767            WC_TRY => self.decode_try(data),
768            _ => DecodedOp::Unknown { code, data },
769        };
770
771        Some(op)
772    }
773
774    fn decode_list(&mut self, data: u32) -> DecodedOp {
775        let list_type = data & ((1 << WC_LIST_FREE) - 1);
776        let is_end = (list_type & Z_END) != 0;
777        let is_simple = (list_type & Z_SIMPLE) != 0;
778        let _skip = data >> WC_LIST_FREE;
779
780        let mut body = Vec::new();
781
782        if is_simple {
783            // Simple list just has a lineno, then the command
784            let lineno = self.next().unwrap_or(0);
785            body.push(DecodedOp::LineNo(lineno));
786        }
787
788        // Continue decoding the list contents
789        if !is_simple {
790            while let Some(wc) = self.peek() {
791                let c = wc_code(wc);
792                if c == WC_END || c == WC_LIST {
793                    break;
794                }
795                if let Some(op) = self.decode_next_op() {
796                    body.push(op);
797                } else {
798                    break;
799                }
800            }
801        }
802
803        DecodedOp::List {
804            list_type,
805            is_end,
806            ops: body,
807        }
808    }
809
810    fn decode_sublist(&mut self, data: u32) -> DecodedOp {
811        let sublist_type = data & 3;
812        let flags = data & 0x1c;
813        let negated = (flags & WC_SUBLIST_NOT) != 0;
814        let is_simple = (flags & WC_SUBLIST_SIMPLE) != 0;
815        let _skip = data >> WC_SUBLIST_FREE;
816
817        let mut body = Vec::new();
818
819        if is_simple {
820            // Simple sublist
821            let lineno = self.next().unwrap_or(0);
822            body.push(DecodedOp::LineNo(lineno));
823        }
824
825        DecodedOp::Sublist {
826            sublist_type,
827            negated,
828            ops: body,
829        }
830    }
831
832    fn decode_pipe(&mut self, data: u32) -> DecodedOp {
833        let pipe_type = data & 1;
834        let lineno = data >> 1;
835        let _is_end = pipe_type == WC_PIPE_END;
836
837        DecodedOp::Pipe {
838            lineno,
839            ops: vec![],
840        }
841    }
842
843    fn decode_redir(&mut self, data: u32) -> DecodedOp {
844        let redir_type = data & 0x1f; // REDIR_TYPE_MASK
845        let has_varid = (data & 0x20) != 0; // REDIR_VARID_MASK
846        let from_heredoc = (data & 0x40) != 0; // REDIR_FROM_HEREDOC_MASK
847
848        let fd = self.next().unwrap_or(0) as i32;
849        let target = self.read_string();
850
851        let varid = if has_varid {
852            Some(self.read_string())
853        } else {
854            None
855        };
856
857        if from_heredoc {
858            // Skip heredoc data (2 extra words)
859            self.next();
860            self.next();
861        }
862
863        DecodedOp::Redir {
864            redir_type,
865            fd,
866            target,
867            varid,
868        }
869    }
870
871    fn decode_assign(&mut self, data: u32) -> DecodedOp {
872        let is_array = (data & 1) != 0;
873        let num_elements = (data >> 2) as usize;
874
875        let name = self.read_string();
876
877        if is_array {
878            let mut values = Vec::with_capacity(num_elements);
879            for _ in 0..num_elements {
880                values.push(self.read_string());
881            }
882            DecodedOp::AssignArray { name, values }
883        } else {
884            let value = self.read_string();
885            DecodedOp::Assign { name, value }
886        }
887    }
888
889    fn decode_simple(&mut self, data: u32) -> DecodedOp {
890        let argc = data as usize;
891        let mut args = Vec::with_capacity(argc);
892        for _ in 0..argc {
893            args.push(self.read_string());
894        }
895        DecodedOp::Simple { args }
896    }
897
898    fn decode_typeset(&mut self, data: u32) -> DecodedOp {
899        let argc = data as usize;
900        let mut args = Vec::with_capacity(argc);
901        for _ in 0..argc {
902            args.push(self.read_string());
903        }
904
905        // Followed by number of assignments
906        let num_assigns = self.next().unwrap_or(0) as usize;
907        let mut assigns = Vec::with_capacity(num_assigns);
908
909        for _ in 0..num_assigns {
910            if let Some(op) = self.decode_next_op() {
911                assigns.push(op);
912            }
913        }
914
915        DecodedOp::Typeset { args, assigns }
916    }
917
918    fn decode_subsh(&mut self, data: u32) -> DecodedOp {
919        let skip = data as usize;
920        let end_pos = self.pos + skip;
921
922        let mut body = Vec::new();
923        while self.pos < end_pos && !self.at_end() {
924            if let Some(op) = self.decode_next_op() {
925                body.push(op);
926            } else {
927                break;
928            }
929        }
930
931        DecodedOp::Subsh { ops: body }
932    }
933
934    fn decode_cursh(&mut self, data: u32) -> DecodedOp {
935        let skip = data as usize;
936        let end_pos = self.pos + skip;
937
938        let mut body = Vec::new();
939        while self.pos < end_pos && !self.at_end() {
940            if let Some(op) = self.decode_next_op() {
941                body.push(op);
942            } else {
943                break;
944            }
945        }
946
947        DecodedOp::Cursh { ops: body }
948    }
949
950    fn decode_timed(&mut self, data: u32) -> DecodedOp {
951        let timed_type = data;
952        let has_pipe = timed_type == 1; // WC_TIMED_PIPE
953
954        if has_pipe {
955            // Followed by a pipe
956            if let Some(op) = self.decode_next_op() {
957                return DecodedOp::Timed {
958                    cmd: Some(Box::new(op)),
959                };
960            }
961        }
962
963        DecodedOp::Timed { cmd: None }
964    }
965
966    fn decode_funcdef(&mut self, data: u32) -> DecodedOp {
967        let skip = data as usize;
968
969        let num_names = self.next().unwrap_or(0) as usize;
970        let mut names = Vec::with_capacity(num_names);
971        for _ in 0..num_names {
972            names.push(self.read_string());
973        }
974
975        // Read function metadata
976        let _strs_offset = self.next();
977        let _strs_len = self.next();
978        let _npats = self.next();
979        let _tracing = self.next();
980
981        // Skip the function body (we'd need a separate decoder for it)
982        let _end_pos = self.pos + skip.saturating_sub(num_names + 5);
983
984        let name = names.first().cloned().unwrap_or_default();
985
986        DecodedOp::FuncDef { name, body: vec![] }
987    }
988
989    fn decode_for(&mut self, data: u32) -> DecodedOp {
990        let for_type = data & 3;
991        let _skip = data >> 2;
992
993        match for_type {
994            WC_FOR_COND => {
995                let init = self.read_string();
996                let cond = self.read_string();
997                let step = self.read_string();
998                DecodedOp::ForCond {
999                    init,
1000                    cond,
1001                    step,
1002                    body: vec![],
1003                }
1004            }
1005            WC_FOR_LIST => {
1006                let var = self.read_string();
1007                let num_words = self.next().unwrap_or(0) as usize;
1008                let mut list = Vec::with_capacity(num_words);
1009                for _ in 0..num_words {
1010                    list.push(self.read_string());
1011                }
1012                DecodedOp::For {
1013                    var,
1014                    list,
1015                    body: vec![],
1016                }
1017            }
1018            _ => {
1019                // WC_FOR_PPARAM - uses positional params
1020                let var = self.read_string();
1021                DecodedOp::For {
1022                    var,
1023                    list: vec![],
1024                    body: vec![],
1025                }
1026            }
1027        }
1028    }
1029
1030    fn decode_select(&mut self, data: u32) -> DecodedOp {
1031        let select_type = data & 1;
1032        let _skip = data >> 1;
1033
1034        let var = self.read_string();
1035        let list = if select_type == 1 {
1036            // WC_SELECT_LIST
1037            let num_words = self.next().unwrap_or(0) as usize;
1038            let mut words = Vec::with_capacity(num_words);
1039            for _ in 0..num_words {
1040                words.push(self.read_string());
1041            }
1042            words
1043        } else {
1044            vec![]
1045        };
1046
1047        DecodedOp::Select {
1048            var,
1049            list,
1050            body: vec![],
1051        }
1052    }
1053
1054    fn decode_while(&mut self, data: u32) -> DecodedOp {
1055        let is_until = (data & 1) != 0;
1056        let _skip = data >> 1;
1057        DecodedOp::While {
1058            cond: vec![],
1059            body: vec![],
1060            is_until,
1061        }
1062    }
1063
1064    fn decode_repeat(&mut self, data: u32) -> DecodedOp {
1065        let _skip = data;
1066        let count = self.read_string();
1067        DecodedOp::Repeat {
1068            count,
1069            body: vec![],
1070        }
1071    }
1072
1073    fn decode_case(&mut self, data: u32) -> DecodedOp {
1074        let case_type = data & 7;
1075        let _skip = data >> WC_CASE_FREE;
1076
1077        if case_type == WC_CASE_HEAD {
1078            let word = self.read_string();
1079            DecodedOp::Case {
1080                word,
1081                cases: vec![],
1082            }
1083        } else {
1084            // Individual case patterns
1085            let pattern = self.read_string();
1086            let _npats = self.next();
1087            DecodedOp::CaseItem {
1088                pattern,
1089                terminator: case_type,
1090                body: vec![],
1091            }
1092        }
1093    }
1094
1095    fn decode_if(&mut self, data: u32) -> DecodedOp {
1096        let if_type = data & 3;
1097        let _skip = data >> 2;
1098
1099        DecodedOp::If {
1100            if_type,
1101            conditions: vec![],
1102            else_body: None,
1103        }
1104    }
1105
1106    fn decode_cond(&mut self, data: u32) -> DecodedOp {
1107        let cond_type = data & 127;
1108        let _skip = data >> 7;
1109
1110        // Decode based on condition type
1111        let args = match cond_type {
1112            // COND_NOT = 1
1113            1 => vec![],
1114            // COND_AND = 2, COND_OR = 3
1115            2 | 3 => vec![],
1116            // Binary operators have 2 args
1117            _ if cond_type >= 7 => {
1118                vec![self.read_string(), self.read_string()]
1119            }
1120            // Unary operators have 1 arg
1121            _ => {
1122                vec![self.read_string()]
1123            }
1124        };
1125
1126        DecodedOp::Cond { cond_type, args }
1127    }
1128
1129    fn decode_arith(&mut self) -> DecodedOp {
1130        let expr = self.read_string();
1131        DecodedOp::Arith { expr }
1132    }
1133
1134    fn decode_try(&mut self, data: u32) -> DecodedOp {
1135        let _skip = data;
1136        DecodedOp::Try {
1137            try_body: vec![],
1138            always_body: vec![],
1139        }
1140    }
1141}
1142
1143/// Dump the function table + header info from a `.zwc` file.
1144/// zshrs-original tooling — C zsh has no `zcompile -t` for this;
1145/// `zsh -c '. file.zwc'` is the only consumer. The `zshrs zwc`
1146/// dump path was added so test scaffolding can inspect zwc layout.
1147pub fn dump_zwc_info<P: AsRef<Path>>(path: P) -> io::Result<()> {
1148    let zwc = ZwcFile::load(&path)?;
1149
1150    println!("ZWC file: {:?}", path.as_ref());
1151    println!(
1152        "  Magic: 0x{:08x} ({})",
1153        zwc.header.magic,
1154        if zwc.header.magic == FD_MAGIC {
1155            "native"
1156        } else {
1157            "swapped"
1158        }
1159    );
1160    println!("  Version: zsh-{}", zwc.header.version);
1161    println!("  Header length: {} words", zwc.header.header_len);
1162    println!("  Wordcode size: {} words", zwc.wordcode.len());
1163    println!("  Functions: {}", zwc.functions.len());
1164
1165    for func in &zwc.functions {
1166        println!(
1167            "    {} (offset={}, len={}, npats={})",
1168            func.name, func.start, func.len, func.npats
1169        );
1170    }
1171
1172    Ok(())
1173}
1174
1175/// Dump one named function's decoded body from a `.zwc` file.
1176/// zshrs-original tooling.
1177pub fn dump_zwc_function<P: AsRef<Path>>(path: P, func_name: &str) -> io::Result<()> {
1178    let zwc = ZwcFile::load(&path)?;
1179
1180    let func = zwc.get_function(func_name).ok_or_else(|| {
1181        io::Error::new(
1182            io::ErrorKind::NotFound,
1183            format!("Function '{}' not found", func_name),
1184        )
1185    })?;
1186
1187    println!("Function: {}", func.name);
1188    println!("  Offset: {} words", func.start);
1189    println!("  Length: {} words", func.len);
1190    println!("  Patterns: {}", func.npats);
1191    println!("  Strings offset: {}", func.strs_offset);
1192
1193    // Show raw wordcode
1194    let header_words = zwc.header.header_len as usize;
1195    let start_idx = (func.start as usize).saturating_sub(header_words);
1196    let end_idx = start_idx + func.len as usize;
1197
1198    if start_idx < zwc.wordcode.len() {
1199        println!("\n  Wordcode:");
1200        let end = end_idx.min(zwc.wordcode.len());
1201        for (i, &wc) in zwc.wordcode[start_idx..end].iter().enumerate().take(50) {
1202            let code = wc_code(wc);
1203            let data = wc_data(wc);
1204            let code_name = match code {
1205                WC_END => "END",
1206                WC_LIST => "LIST",
1207                WC_SUBLIST => "SUBLIST",
1208                WC_PIPE => "PIPE",
1209                WC_REDIR => "REDIR",
1210                WC_ASSIGN => "ASSIGN",
1211                WC_SIMPLE => "SIMPLE",
1212                WC_TYPESET => "TYPESET",
1213                WC_SUBSH => "SUBSH",
1214                WC_CURSH => "CURSH",
1215                WC_TIMED => "TIMED",
1216                WC_FUNCDEF => "FUNCDEF",
1217                WC_FOR => "FOR",
1218                WC_SELECT => "SELECT",
1219                WC_WHILE => "WHILE",
1220                WC_REPEAT => "REPEAT",
1221                WC_CASE => "CASE",
1222                WC_IF => "IF",
1223                WC_COND => "COND",
1224                WC_ARITH => "ARITH",
1225                WC_AUTOFN => "AUTOFN",
1226                WC_TRY => "TRY",
1227                _ => "???",
1228            };
1229            println!("    [{:3}] 0x{:08x} = {} (data={})", i, wc, code_name, data);
1230        }
1231        if end - start_idx > 50 {
1232            println!("    ... ({} more words)", end - start_idx - 50);
1233        }
1234    }
1235
1236    // Try to decode
1237    if let Some(decoded) = zwc.decode_function(func) {
1238        println!("\n  Decoded ops:");
1239        for (i, op) in decoded.body.iter().enumerate().take(20) {
1240            println!("    [{:2}] {:?}", i, op);
1241        }
1242        if decoded.body.len() > 20 {
1243            println!("    ... ({} more ops)", decoded.body.len() - 20);
1244        }
1245    }
1246
1247    Ok(())
1248}
1249
1250/// Convert decoded ZWC ops to our shell AST for execution
1251impl DecodedOp {
1252    /// `to_shell_command` — see implementation.
1253    pub fn to_shell_command(&self) -> Option<ShellCommand> {
1254        match self {
1255            DecodedOp::Simple { args } => {
1256                if args.is_empty() {
1257                    return None;
1258                }
1259                Some(ShellCommand::Simple(SimpleCommand {
1260                    assignments: vec![],
1261                    words: args.iter().map(|s| ShellWord::Literal(s.clone())).collect(),
1262                    redirects: vec![],
1263                }))
1264            }
1265
1266            DecodedOp::Assign { name, value } => Some(ShellCommand::Simple(SimpleCommand {
1267                assignments: vec![(name.clone(), ShellWord::Literal(value.clone()), false)],
1268                words: vec![],
1269                redirects: vec![],
1270            })),
1271
1272            DecodedOp::AssignArray { name, values } => {
1273                let array_word = ShellWord::Concat(
1274                    values
1275                        .iter()
1276                        .map(|s| ShellWord::Literal(s.clone()))
1277                        .collect(),
1278                );
1279                Some(ShellCommand::Simple(SimpleCommand {
1280                    assignments: vec![(name.clone(), array_word, false)],
1281                    words: vec![],
1282                    redirects: vec![],
1283                }))
1284            }
1285
1286            DecodedOp::List { ops, .. } => {
1287                let commands: Vec<(ShellCommand, ListOp)> = ops
1288                    .iter()
1289                    .filter_map(|op| op.to_shell_command())
1290                    .map(|cmd| (cmd, ListOp::Semi))
1291                    .collect();
1292
1293                if commands.is_empty() {
1294                    None
1295                } else if commands.len() == 1 {
1296                    Some(commands.into_iter().next().unwrap().0)
1297                } else {
1298                    Some(ShellCommand::List(commands))
1299                }
1300            }
1301
1302            DecodedOp::Sublist { ops, negated, .. } => {
1303                let commands: Vec<ShellCommand> =
1304                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1305
1306                if commands.is_empty() {
1307                    None
1308                } else {
1309                    Some(ShellCommand::Pipeline(commands, *negated))
1310                }
1311            }
1312
1313            DecodedOp::Pipe { ops, .. } => {
1314                let commands: Vec<ShellCommand> =
1315                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1316
1317                if commands.is_empty() {
1318                    None
1319                } else if commands.len() == 1 {
1320                    Some(commands.into_iter().next().unwrap())
1321                } else {
1322                    Some(ShellCommand::Pipeline(commands, false))
1323                }
1324            }
1325
1326            DecodedOp::Typeset { args, assigns } => {
1327                // Typeset is like a simple command with the typeset builtin
1328                let mut words: Vec<ShellWord> =
1329                    args.iter().map(|s| ShellWord::Literal(s.clone())).collect();
1330
1331                // Add any assignments as words
1332                for assign in assigns {
1333                    if let DecodedOp::Assign { name, value } = assign {
1334                        words.push(ShellWord::Literal(format!("{}={}", name, value)));
1335                    }
1336                }
1337
1338                Some(ShellCommand::Simple(SimpleCommand {
1339                    assignments: vec![],
1340                    words,
1341                    redirects: vec![],
1342                }))
1343            }
1344
1345            DecodedOp::Subsh { ops } => {
1346                let commands: Vec<ShellCommand> =
1347                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1348                Some(ShellCommand::Compound(CompoundCommand::Subshell(commands)))
1349            }
1350
1351            DecodedOp::Cursh { ops } => {
1352                let commands: Vec<ShellCommand> =
1353                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1354                Some(ShellCommand::Compound(CompoundCommand::BraceGroup(
1355                    commands,
1356                )))
1357            }
1358
1359            DecodedOp::For { var, list, body } => {
1360                let words = if list.is_empty() {
1361                    None
1362                } else {
1363                    Some(list.iter().map(|s| ShellWord::Literal(s.clone())).collect())
1364                };
1365                let body_cmds: Vec<ShellCommand> =
1366                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1367                Some(ShellCommand::Compound(CompoundCommand::For {
1368                    var: var.clone(),
1369                    words,
1370                    body: body_cmds,
1371                }))
1372            }
1373
1374            DecodedOp::ForCond {
1375                init,
1376                cond,
1377                step,
1378                body,
1379            } => {
1380                let body_cmds: Vec<ShellCommand> =
1381                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1382                Some(ShellCommand::Compound(CompoundCommand::ForArith {
1383                    init: init.clone(),
1384                    cond: cond.clone(),
1385                    step: step.clone(),
1386                    body: body_cmds,
1387                }))
1388            }
1389
1390            DecodedOp::While {
1391                cond,
1392                body,
1393                is_until,
1394            } => {
1395                let cond_cmds: Vec<ShellCommand> =
1396                    cond.iter().filter_map(|op| op.to_shell_command()).collect();
1397                let body_cmds: Vec<ShellCommand> =
1398                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1399
1400                if *is_until {
1401                    Some(ShellCommand::Compound(CompoundCommand::Until {
1402                        condition: cond_cmds,
1403                        body: body_cmds,
1404                    }))
1405                } else {
1406                    Some(ShellCommand::Compound(CompoundCommand::While {
1407                        condition: cond_cmds,
1408                        body: body_cmds,
1409                    }))
1410                }
1411            }
1412
1413            DecodedOp::FuncDef { name, body } => {
1414                let body_cmds: Vec<ShellCommand> =
1415                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1416
1417                let func_body = if body_cmds.is_empty() {
1418                    // Empty function body - create a no-op
1419                    ShellCommand::Simple(SimpleCommand {
1420                        assignments: vec![],
1421                        words: vec![ShellWord::Literal(":".to_string())],
1422                        redirects: vec![],
1423                    })
1424                } else if body_cmds.len() == 1 {
1425                    body_cmds.into_iter().next().unwrap()
1426                } else {
1427                    ShellCommand::List(body_cmds.into_iter().map(|c| (c, ListOp::Semi)).collect())
1428                };
1429
1430                Some(ShellCommand::FunctionDef(name.clone(), Box::new(func_body)))
1431            }
1432
1433            DecodedOp::Arith { expr } => {
1434                Some(ShellCommand::Compound(CompoundCommand::Arith(expr.clone())))
1435            }
1436
1437            // Ops that don't directly translate
1438            DecodedOp::End | DecodedOp::LineNo(_) | DecodedOp::AutoFn => None,
1439
1440            DecodedOp::Redir { .. } => {
1441                // Redirections are attached to commands, not standalone
1442                None
1443            }
1444
1445            DecodedOp::If {
1446                conditions,
1447                else_body,
1448                ..
1449            } => {
1450                let cond_pairs: Vec<(Vec<ShellCommand>, Vec<ShellCommand>)> = conditions
1451                    .iter()
1452                    .map(|(c, b)| {
1453                        (
1454                            c.iter().filter_map(|op| op.to_shell_command()).collect(),
1455                            b.iter().filter_map(|op| op.to_shell_command()).collect(),
1456                        )
1457                    })
1458                    .collect();
1459                let else_part: Option<Vec<ShellCommand>> = else_body
1460                    .as_ref()
1461                    .map(|body| body.iter().filter_map(|op| op.to_shell_command()).collect());
1462                Some(ShellCommand::Compound(CompoundCommand::If {
1463                    conditions: cond_pairs,
1464                    else_part,
1465                }))
1466            }
1467
1468            DecodedOp::Case { word, cases } => {
1469                let mapped: Vec<(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)> = cases
1470                    .iter()
1471                    .map(|(pat, body)| {
1472                        (
1473                            vec![ShellWord::Literal(pat.clone())],
1474                            body.iter().filter_map(|op| op.to_shell_command()).collect(),
1475                            CaseTerminator::Break,
1476                        )
1477                    })
1478                    .collect();
1479                Some(ShellCommand::Compound(CompoundCommand::Case {
1480                    word: ShellWord::Literal(word.clone()),
1481                    cases: mapped,
1482                }))
1483            }
1484
1485            DecodedOp::CaseItem { .. } => {
1486                // CaseItem is only meaningful as a child of Case; the Case
1487                // branch above flattens directly from the (pattern, body)
1488                // pairs the decoder builds, so a stray CaseItem at the top
1489                // level has no executable form.
1490                None
1491            }
1492
1493            DecodedOp::Repeat { count, body } => {
1494                let body_cmds: Vec<ShellCommand> =
1495                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1496                Some(ShellCommand::Compound(CompoundCommand::Repeat {
1497                    count: count.clone(),
1498                    body: body_cmds,
1499                }))
1500            }
1501
1502            DecodedOp::Try {
1503                try_body,
1504                always_body,
1505            } => {
1506                let try_cmds: Vec<ShellCommand> = try_body
1507                    .iter()
1508                    .filter_map(|op| op.to_shell_command())
1509                    .collect();
1510                let always_cmds: Vec<ShellCommand> = always_body
1511                    .iter()
1512                    .filter_map(|op| op.to_shell_command())
1513                    .collect();
1514                Some(ShellCommand::Compound(CompoundCommand::Try {
1515                    try_body: try_cmds,
1516                    always_body: always_cmds,
1517                }))
1518            }
1519
1520            DecodedOp::Select { .. } => {
1521                // CompoundCommand::Select needs a var and word list; the
1522                // current DecodedOp::Select carries fields the decoder
1523                // hasn't surfaced yet (see zwc.rs:1054-1086 for the parts
1524                // we do decode). Leave unmapped until the decoder grows
1525                // those fields rather than guess at them here.
1526                None
1527            }
1528
1529            DecodedOp::Cond { .. } => {
1530                // [[ ... ]] conditional. CompoundCommand has no Cond variant —
1531                // the parser-level ZshCond shape lives only in the parse
1532                // crate. Bridging that requires a converter on the parse
1533                // side; deferred to a follow-up port.
1534                None
1535            }
1536
1537            DecodedOp::Timed { .. } => {
1538                // `time cmd` — needs ZshCommand::Time-style wrapping which
1539                // CompoundCommand doesn't model. Deferred.
1540                None
1541            }
1542
1543            DecodedOp::Unknown { .. } => None,
1544        }
1545    }
1546}
1547
1548/// Helper to convert redir type to our RedirectOp
1549#[allow(dead_code)]
1550fn redir_type_to_op(redir_type: u32) -> Option<RedirectOp> {
1551    // Zsh redirect types from zsh.h
1552    const REDIR_WRITE: u32 = 0;
1553    const REDIR_WRITENOW: u32 = 1;
1554    const REDIR_APP: u32 = 2;
1555    const REDIR_APPNOW: u32 = 3;
1556    const REDIR_ERRWRITE: u32 = 4;
1557    const REDIR_ERRWRITENOW: u32 = 5;
1558    const REDIR_ERRAPP: u32 = 6;
1559    const REDIR_ERRAPPNOW: u32 = 7;
1560    const REDIR_READWRITE: u32 = 8;
1561    const REDIR_READ: u32 = 9;
1562    const REDIR_HEREDOC: u32 = 10;
1563    const REDIR_HEREDOCDASH: u32 = 11;
1564    const REDIR_HERESTR: u32 = 12;
1565    const REDIR_MERGEIN: u32 = 13;
1566    const REDIR_MERGEOUT: u32 = 14;
1567    const REDIR_CLOSE: u32 = 15;
1568    const REDIR_INPIPE: u32 = 16;
1569    const REDIR_OUTPIPE: u32 = 17;
1570
1571    match redir_type {
1572        REDIR_WRITE | REDIR_WRITENOW => Some(RedirectOp::Write),
1573        REDIR_APP | REDIR_APPNOW => Some(RedirectOp::Append),
1574        REDIR_ERRWRITE | REDIR_ERRWRITENOW => Some(RedirectOp::WriteBoth),
1575        REDIR_ERRAPP | REDIR_ERRAPPNOW => Some(RedirectOp::AppendBoth),
1576        REDIR_READWRITE => Some(RedirectOp::ReadWrite),
1577        REDIR_READ => Some(RedirectOp::Read),
1578        REDIR_HEREDOC | REDIR_HEREDOCDASH => Some(RedirectOp::HereDoc),
1579        REDIR_HERESTR => Some(RedirectOp::HereString),
1580        REDIR_MERGEIN => Some(RedirectOp::DupRead),
1581        REDIR_MERGEOUT => Some(RedirectOp::DupWrite),
1582        REDIR_CLOSE | REDIR_INPIPE | REDIR_OUTPIPE => None, // Not directly supported
1583        _ => None,
1584    }
1585}
1586
1587impl DecodedFunction {
1588    /// Convert the decoded function to a shell function definition
1589    pub fn to_shell_function(&self) -> Option<ShellCommand> {
1590        let body_cmds: Vec<ShellCommand> = self
1591            .body
1592            .iter()
1593            .filter_map(|op| op.to_shell_command())
1594            .collect();
1595
1596        let func_body = if body_cmds.is_empty() {
1597            ShellCommand::Simple(SimpleCommand {
1598                assignments: vec![],
1599                words: vec![ShellWord::Literal(":".to_string())],
1600                redirects: vec![],
1601            })
1602        } else if body_cmds.len() == 1 {
1603            body_cmds.into_iter().next().unwrap()
1604        } else {
1605            ShellCommand::List(body_cmds.into_iter().map(|c| (c, ListOp::Semi)).collect())
1606        };
1607
1608        // Extract just the function name without the path prefix
1609        let name = self
1610            .name
1611            .rsplit('/')
1612            .next()
1613            .unwrap_or(&self.name)
1614            .to_string();
1615
1616        Some(ShellCommand::FunctionDef(name, Box::new(func_body)))
1617    }
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622    use super::*;
1623
1624    #[test]
1625    fn test_wc_code() {
1626        let _g = crate::test_util::global_state_lock();
1627        assert_eq!(wc_code(WC_LIST), WC_LIST);
1628        assert_eq!(wc_code(WC_SIMPLE | (5 << WC_CODEBITS)), WC_SIMPLE);
1629    }
1630
1631    #[test]
1632    fn test_wc_data() {
1633        let _g = crate::test_util::global_state_lock();
1634        let wc = WC_SIMPLE | (42 << WC_CODEBITS);
1635        assert_eq!(wc_data(wc), 42);
1636    }
1637
1638    #[test]
1639    fn test_load_src_zwc() {
1640        let _g = crate::test_util::global_state_lock();
1641        let path = "/Users/wizard/.zinit/plugins/MenkeTechnologies---zsh-more-completions/src.zwc";
1642        if !std::path::Path::new(path).exists() {
1643            eprintln!("Skipping test - {} not found", path);
1644            return;
1645        }
1646
1647        let zwc = ZwcFile::load(path).expect("Failed to load src.zwc");
1648        println!("Loaded {} functions from src.zwc", zwc.function_count());
1649
1650        // Should have thousands of completion functions
1651        assert!(
1652            zwc.function_count() > 1000,
1653            "Expected > 1000 functions, got {}",
1654            zwc.function_count()
1655        );
1656
1657        // Check some known functions exist
1658        let funcs = zwc.list_functions();
1659        println!("First 10 functions: {:?}", &funcs[..10.min(funcs.len())]);
1660
1661        // Try to decode _ls
1662        if let Some(func) = zwc.get_function("_ls") {
1663            println!("Found _ls function");
1664            if let Some(decoded) = zwc.decode_function(func) {
1665                println!("Decoded _ls: {} ops", decoded.body.len());
1666            }
1667        }
1668    }
1669
1670    #[test]
1671    fn test_load_zshrc_zwc() {
1672        let _g = crate::test_util::global_state_lock();
1673        let home = std::env::var("HOME").unwrap_or_default();
1674        let path = format!("{}/.zshrc.zwc", home);
1675        if !std::path::Path::new(&path).exists() {
1676            eprintln!("Skipping test - {} not found", path);
1677            return;
1678        }
1679
1680        let zwc = ZwcFile::load(&path).expect("Failed to load .zshrc.zwc");
1681        println!("Loaded {} functions from .zshrc.zwc", zwc.function_count());
1682
1683        for name in zwc.list_functions() {
1684            println!("  Function: {}", name);
1685            if let Some(func) = zwc.get_function(name) {
1686                if let Some(decoded) = zwc.decode_function(func) {
1687                    println!("    Decoded: {} ops", decoded.body.len());
1688                    for (i, op) in decoded.body.iter().take(3).enumerate() {
1689                        if let Some(cmd) = op.to_shell_command() {
1690                            println!("      [{}] -> ShellCommand OK", i);
1691                        } else {
1692                            println!("      [{}] {:?}", i, op);
1693                        }
1694                    }
1695                }
1696            }
1697        }
1698    }
1699
1700    #[test]
1701    fn decoded_op_if_converts_to_compound_if() {
1702        let _g = crate::test_util::global_state_lock();
1703        let cmd_a = DecodedOp::Simple {
1704            args: vec!["true".into()],
1705        };
1706        let cmd_b = DecodedOp::Simple {
1707            args: vec!["false".into()],
1708        };
1709        let op = DecodedOp::If {
1710            if_type: 0,
1711            conditions: vec![(vec![cmd_a], vec![cmd_b])],
1712            else_body: None,
1713        };
1714        let result = op.to_shell_command();
1715        match result {
1716            Some(ShellCommand::Compound(CompoundCommand::If {
1717                conditions,
1718                else_part,
1719            })) => {
1720                assert_eq!(conditions.len(), 1);
1721                assert!(else_part.is_none());
1722                assert_eq!(conditions[0].0.len(), 1);
1723                assert_eq!(conditions[0].1.len(), 1);
1724            }
1725            other => panic!("expected If, got {:?}", other),
1726        }
1727    }
1728
1729    #[test]
1730    fn decoded_op_repeat_converts_with_count_and_body() {
1731        let _g = crate::test_util::global_state_lock();
1732        let body = DecodedOp::Simple {
1733            args: vec!["echo".into(), "hi".into()],
1734        };
1735        let op = DecodedOp::Repeat {
1736            count: "3".into(),
1737            body: vec![body],
1738        };
1739        match op.to_shell_command() {
1740            Some(ShellCommand::Compound(CompoundCommand::Repeat { count, body })) => {
1741                assert_eq!(count, "3");
1742                assert_eq!(body.len(), 1);
1743            }
1744            other => panic!("expected Repeat, got {:?}", other),
1745        }
1746    }
1747
1748    #[test]
1749    fn decoded_op_case_converts_each_pattern_branch() {
1750        let _g = crate::test_util::global_state_lock();
1751        let body_one = DecodedOp::Simple {
1752            args: vec!["echo".into(), "one".into()],
1753        };
1754        let body_two = DecodedOp::Simple {
1755            args: vec!["echo".into(), "two".into()],
1756        };
1757        let op = DecodedOp::Case {
1758            word: "$x".into(),
1759            cases: vec![("a*".into(), vec![body_one]), ("b*".into(), vec![body_two])],
1760        };
1761        match op.to_shell_command() {
1762            Some(ShellCommand::Compound(CompoundCommand::Case { cases, .. })) => {
1763                assert_eq!(cases.len(), 2);
1764                assert_eq!(cases[0].0.len(), 1);
1765                assert_eq!(cases[1].0.len(), 1);
1766            }
1767            other => panic!("expected Case, got {:?}", other),
1768        }
1769    }
1770
1771    #[test]
1772    fn decoded_op_try_converts_both_arms() {
1773        let _g = crate::test_util::global_state_lock();
1774        let try_arm = DecodedOp::Simple {
1775            args: vec!["false".into()],
1776        };
1777        let always_arm = DecodedOp::Simple {
1778            args: vec!["echo".into(), "done".into()],
1779        };
1780        let op = DecodedOp::Try {
1781            try_body: vec![try_arm],
1782            always_body: vec![always_arm],
1783        };
1784        match op.to_shell_command() {
1785            Some(ShellCommand::Compound(CompoundCommand::Try {
1786                try_body,
1787                always_body,
1788            })) => {
1789                assert_eq!(try_body.len(), 1);
1790                assert_eq!(always_body.len(), 1);
1791            }
1792            other => panic!("expected Try, got {:?}", other),
1793        }
1794    }
1795
1796    #[test]
1797    fn test_load_zshenv_zwc() {
1798        let _g = crate::test_util::global_state_lock();
1799        let home = std::env::var("HOME").unwrap_or_default();
1800        let path = format!("{}/.zshenv.zwc", home);
1801        if !std::path::Path::new(&path).exists() {
1802            eprintln!("Skipping test - {} not found", path);
1803            return;
1804        }
1805
1806        let zwc = ZwcFile::load(&path).expect("Failed to load .zshenv.zwc");
1807        println!("Loaded {} functions from .zshenv.zwc", zwc.function_count());
1808
1809        for name in zwc.list_functions() {
1810            println!("  Function: {}", name);
1811            if let Some(func) = zwc.get_function(name) {
1812                if let Some(decoded) = zwc.decode_function(func) {
1813                    println!("    Decoded: {} ops", decoded.body.len());
1814                }
1815            }
1816        }
1817    }
1818}