Skip to main content

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