Skip to main content

zsh/
zwc.rs

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