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