1use crate::parse::{
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;
35use std::io::Write;
36
37const FD_MAGIC: u32 = 0x04050607;
38const FD_OMAGIC: u32 = 0x07060504; const FD_PRELEN: usize = 12;
40use crate::ported::zsh_h::{
41 WC_END, WC_LIST, WC_SUBLIST, WC_PIPE, WC_REDIR, WC_ASSIGN, WC_SIMPLE, WC_TYPESET,
42 WC_SUBSH, WC_CURSH, WC_TIMED, WC_FUNCDEF, WC_FOR, WC_SELECT, WC_WHILE, WC_REPEAT,
43 WC_CASE, WC_IF, WC_COND, WC_ARITH, WC_AUTOFN, WC_TRY,
44 WC_LIST_FREE, WC_SUBLIST_FREE, WC_CASE_FREE,
45 WC_SUBLIST_END, WC_SUBLIST_AND, WC_SUBLIST_OR, WC_SUBLIST_COPROC, WC_SUBLIST_NOT,
46 WC_SUBLIST_SIMPLE,
47 WC_PIPE_END, WC_PIPE_MID,
48 WC_ASSIGN_SCALAR, WC_ASSIGN_ARRAY, WC_ASSIGN_INC,
49 WC_FOR_PPARAM, WC_FOR_LIST, WC_FOR_COND,
50 WC_SELECT_PPARAM, WC_SELECT_LIST,
51 WC_WHILE_WHILE, WC_WHILE_UNTIL,
52 WC_CODEBITS,
53 WC_CASE_HEAD, WC_CASE_OR, WC_CASE_AND, WC_CASE_TESTAND,
54 WC_IF_HEAD, WC_IF_IF, WC_IF_ELIF, WC_IF_ELSE,
55 Pound, Hat, Star, Inpar, Outpar, Equals, Bar, Inbrace, Outbrace, Inbrack,
56 Stringg, Outbrack, Tick, Inang, Outang, Quest, Tilde, Comma, Dash, Bang,
57 Snull, Dnull, Bnull, Nularg,
58};
59const Z_END: u32 = crate::ported::zsh_h::Z_END as u32;
62const Z_SIMPLE: u32 = crate::ported::zsh_h::Z_SIMPLE as u32;
63
64pub(crate) fn untokenize(bytes: &[u8]) -> String {
66 let mut result = String::new();
67 let mut i = 0;
68
69 while i < bytes.len() {
70 let b = bytes[i];
71 let c = b as char;
75 match c {
76 Pound => result.push('#'),
77 Stringg => result.push('$'),
78 Hat => result.push('^'),
79 Star => result.push('*'),
80 Inpar => result.push('('),
81 Outpar => result.push(')'),
82 Equals => result.push('='),
83 Bar => result.push('|'),
84 Inbrace => result.push('{'),
85 Outbrace => result.push('}'),
86 Inbrack => result.push('['),
87 Outbrack => result.push(']'),
88 Tick => result.push('`'),
89 Inang => result.push('<'),
90 Outang => result.push('>'),
91 Quest => result.push('?'),
92 Tilde => result.push('~'),
93 Comma => result.push(','),
94 Dash => result.push('-'),
95 Bang => result.push('!'),
96 Snull | Dnull | Bnull | Nularg => {
97 }
99 '\u{89}' => result.push_str("(("), '\u{8b}' => result.push_str("))"), _ if b >= 0x80 => {
102 }
104 _ => result.push(c),
105 }
106 i += 1;
107 }
108
109 result
110}
111
112#[inline]
115pub fn wc_code(c: u32) -> u32 {
116 c & ((1 << WC_CODEBITS) - 1)
117}
118
119#[inline]
122pub fn wc_data(c: u32) -> u32 {
123 c >> WC_CODEBITS
124}
125
126#[derive(Debug)]
131pub struct ZwcHeader {
132 pub magic: u32,
133 pub flags: u8,
134 pub version: String,
135 pub header_len: u32,
136 pub other_offset: u32,
137}
138
139#[derive(Debug)]
144pub struct ZwcFunction {
145 pub name: String,
146 pub start: u32,
147 pub len: u32,
148 pub npats: u32,
149 pub strs_offset: u32,
150 pub flags: u32,
151}
152
153#[derive(Debug)]
158pub struct ZwcFile {
159 pub header: ZwcHeader,
160 pub functions: Vec<ZwcFunction>,
161 pub wordcode: Vec<u32>,
162 pub strings: Vec<u8>,
163}
164
165impl ZwcFile {
166 pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
167 let mut file = File::open(path)?;
168 let mut buf = vec![0u8; (FD_PRELEN + 1) * 4];
169
170 file.read_exact(&mut buf)?;
171
172 let magic = u32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
173
174 let swap_bytes = if magic == FD_MAGIC {
175 false
176 } else if magic == FD_OMAGIC {
177 true
178 } else {
179 return Err(io::Error::new(
180 io::ErrorKind::InvalidData,
181 format!("Invalid ZWC magic: 0x{:08x}", magic),
182 ));
183 };
184
185 let read_u32 = |bytes: &[u8], offset: usize| -> u32 {
186 let b = &bytes[offset..offset + 4];
187 let val = u32::from_ne_bytes([b[0], b[1], b[2], b[3]]);
188 if swap_bytes {
189 val.swap_bytes()
190 } else {
191 val
192 }
193 };
194
195 let flags = buf[4];
196 let other_offset = (buf[5] as u32) | ((buf[6] as u32) << 8) | ((buf[7] as u32) << 16);
197
198 let version_start = 8;
200 let version_end = buf[version_start..]
201 .iter()
202 .position(|&b| b == 0)
203 .map(|p| version_start + p)
204 .unwrap_or(buf.len());
205 let version = String::from_utf8_lossy(&buf[version_start..version_end]).to_string();
206
207 let header_len = read_u32(&buf, FD_PRELEN * 4);
208
209 let header = ZwcHeader {
210 magic,
211 flags,
212 version,
213 header_len,
214 other_offset,
215 };
216
217 file.seek(SeekFrom::Start(0))?;
219 let full_header_size = (header_len as usize) * 4;
220 let mut header_buf = vec![0u8; full_header_size];
221 file.read_exact(&mut header_buf)?;
222
223 let mut functions = Vec::new();
225 let mut offset = FD_PRELEN * 4;
226
227 while offset < full_header_size {
228 if offset + 24 > full_header_size {
229 break;
230 }
231
232 let start = read_u32(&header_buf, offset);
233 let len = read_u32(&header_buf, offset + 4);
234 let npats = read_u32(&header_buf, offset + 8);
235 let strs = read_u32(&header_buf, offset + 12);
236 let hlen = read_u32(&header_buf, offset + 16);
237 let flags = read_u32(&header_buf, offset + 20);
238
239 let name_start = offset + 24;
241 let name_end = header_buf[name_start..]
242 .iter()
243 .position(|&b| b == 0)
244 .map(|p| name_start + p)
245 .unwrap_or(full_header_size);
246
247 let name = String::from_utf8_lossy(&header_buf[name_start..name_end]).to_string();
248
249 if name.is_empty() {
250 break;
251 }
252
253 functions.push(ZwcFunction {
254 name,
255 start,
256 len,
257 npats,
258 strs_offset: strs,
259 flags,
260 });
261
262 offset += (hlen as usize) * 4;
264 }
265
266 let mut rest = Vec::new();
268 file.read_to_end(&mut rest)?;
269
270 let mut wordcode = Vec::new();
272 let mut i = 0;
273 while i + 4 <= rest.len() {
274 let val = u32::from_ne_bytes([rest[i], rest[i + 1], rest[i + 2], rest[i + 3]]);
275 wordcode.push(if swap_bytes { val.swap_bytes() } else { val });
276 i += 4;
277 }
278
279 let strings = rest;
281
282 Ok(ZwcFile {
283 header,
284 functions,
285 wordcode,
286 strings,
287 })
288 }
289
290 pub fn list_functions(&self) -> Vec<&str> {
291 self.functions.iter().map(|f| f.name.as_str()).collect()
292 }
293
294 pub fn function_count(&self) -> usize {
295 self.functions.len()
296 }
297
298 pub fn new_builder() -> ZwcBuilder {
300 ZwcBuilder::new()
301 }
302
303 pub fn get_function(&self, name: &str) -> Option<&ZwcFunction> {
304 self.functions
305 .iter()
306 .find(|f| f.name == name || f.name.ends_with(&format!("/{}", name)))
307 }
308
309 pub fn decode_function(&self, func: &ZwcFunction) -> Option<DecodedFunction> {
310 let header_words = self.header.header_len as usize;
311 let start_idx = (func.start as usize).saturating_sub(header_words);
312
313 if start_idx >= self.wordcode.len() {
314 return None;
315 }
316
317 let func_wordcode = &self.wordcode[start_idx..];
320
321 let mut string_bytes = Vec::new();
324 for &wc in func_wordcode {
325 string_bytes.extend_from_slice(&wc.to_ne_bytes());
326 }
327
328 let decoder = WordcodeDecoder::new(func_wordcode, &string_bytes, func.strs_offset as usize);
329
330 Some(DecodedFunction {
331 name: func.name.clone(),
332 body: decoder.decode(),
333 })
334 }
335}
336
337#[derive(Debug)]
342pub struct ZwcBuilder {
343 functions: Vec<(String, Vec<u8>)>, }
345
346impl Default for ZwcBuilder {
347 fn default() -> Self {
348 Self::new()
349 }
350}
351
352impl ZwcBuilder {
353 pub fn new() -> Self {
354 Self {
355 functions: Vec::new(),
356 }
357 }
358
359 pub fn add_source(&mut self, name: &str, source: &str) {
361 self.functions
362 .push((name.to_string(), source.as_bytes().to_vec()));
363 }
364
365 pub fn add_file(&mut self, path: &std::path::Path) -> io::Result<()> {
367 let name = path
368 .file_name()
369 .and_then(|n| n.to_str())
370 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid filename"))?;
371 let source = std::fs::read(path)?;
372 self.functions.push((name.to_string(), source));
373 Ok(())
374 }
375
376 pub fn write<P: AsRef<std::path::Path>>(&self, path: P) -> io::Result<()> {
380
381 let mut file = std::fs::File::create(path)?;
382
383 file.write_all(&FD_MAGIC.to_ne_bytes())?;
385
386 file.write_all(&[0u8])?;
388
389 file.write_all(&[0u8; 3])?;
391
392 let version = env!("CARGO_PKG_VERSION");
394 let version_bytes = version.as_bytes();
395 file.write_all(version_bytes)?;
396 file.write_all(&[0u8])?; let padding = (4 - ((version_bytes.len() + 1) % 4)) % 4;
399 file.write_all(&vec![0u8; padding])?;
400
401 let mut header_words = FD_PRELEN;
403 for (name, _) in &self.functions {
404 header_words += 6 + (name.len() + 1).div_ceil(4);
406 }
407
408 file.write_all(&(header_words as u32).to_ne_bytes())?;
410
411 let mut data_offset = header_words;
413 let mut func_data: Vec<(u32, u32, Vec<u8>)> = Vec::new(); for (name, source) in &self.functions {
417 let source_words = source.len().div_ceil(4);
418
419 file.write_all(&(data_offset as u32).to_ne_bytes())?; file.write_all(&(source.len() as u32).to_ne_bytes())?; file.write_all(&0u32.to_ne_bytes())?; file.write_all(&0u32.to_ne_bytes())?; let hlen = 6 + (name.len() + 1).div_ceil(4);
425 file.write_all(&(hlen as u32).to_ne_bytes())?; file.write_all(&0u32.to_ne_bytes())?; file.write_all(name.as_bytes())?;
430 file.write_all(&[0u8])?;
431 let name_padding = (4 - ((name.len() + 1) % 4)) % 4;
432 file.write_all(&vec![0u8; name_padding])?;
433
434 func_data.push((data_offset as u32, source.len() as u32, source.clone()));
435 data_offset += source_words;
436 }
437
438 for (_, _, data) in &func_data {
440 file.write_all(data)?;
441 let padding = (4 - (data.len() % 4)) % 4;
442 file.write_all(&vec![0u8; padding])?;
443 }
444
445 Ok(())
446 }
447}
448
449#[derive(Debug, Clone)]
454pub struct DecodedFunction {
455 pub name: String,
456 pub body: Vec<DecodedOp>,
457}
458
459#[derive(Debug, Clone)]
465pub enum DecodedOp {
466 End,
467 LineNo(u32),
468 List {
469 list_type: u32,
470 is_end: bool,
471 ops: Vec<DecodedOp>,
472 },
473 Sublist {
474 sublist_type: u32,
475 negated: bool,
476 ops: Vec<DecodedOp>,
477 },
478 Pipe {
479 lineno: u32,
480 ops: Vec<DecodedOp>,
481 },
482 Redir {
483 redir_type: u32,
484 fd: i32,
485 target: String,
486 varid: Option<String>,
487 },
488 Assign {
489 name: String,
490 value: String,
491 },
492 AssignArray {
493 name: String,
494 values: Vec<String>,
495 },
496 Simple {
497 args: Vec<String>,
498 },
499 Typeset {
500 args: Vec<String>,
501 assigns: Vec<DecodedOp>,
502 },
503 Subsh {
504 ops: Vec<DecodedOp>,
505 },
506 Cursh {
507 ops: Vec<DecodedOp>,
508 },
509 Timed {
510 cmd: Option<Box<DecodedOp>>,
511 },
512 FuncDef {
513 name: String,
514 body: Vec<DecodedOp>,
515 },
516 For {
517 var: String,
518 list: Vec<String>,
519 body: Vec<DecodedOp>,
520 },
521 ForCond {
522 init: String,
523 cond: String,
524 step: String,
525 body: Vec<DecodedOp>,
526 },
527 Select {
528 var: String,
529 list: Vec<String>,
530 body: Vec<DecodedOp>,
531 },
532 While {
533 cond: Vec<DecodedOp>,
534 body: Vec<DecodedOp>,
535 is_until: bool,
536 },
537 Repeat {
538 count: String,
539 body: Vec<DecodedOp>,
540 },
541 Case {
542 word: String,
543 cases: Vec<(String, Vec<DecodedOp>)>,
544 },
545 CaseItem {
546 pattern: String,
547 terminator: u32,
548 body: Vec<DecodedOp>,
549 },
550 If {
551 if_type: u32,
552 conditions: Vec<(Vec<DecodedOp>, Vec<DecodedOp>)>,
553 else_body: Option<Vec<DecodedOp>>,
554 },
555 Cond {
556 cond_type: u32,
557 args: Vec<String>,
558 },
559 Arith {
560 expr: String,
561 },
562 AutoFn,
563 Try {
564 try_body: Vec<DecodedOp>,
565 always_body: Vec<DecodedOp>,
566 },
567 Unknown {
568 code: u32,
569 data: u32,
570 },
571}
572
573pub struct WordcodeDecoder<'a> {
578 code: &'a [u32],
579 strings: &'a [u8],
580 strs_base: usize,
581 pub pos: usize,
582}
583
584impl<'a> WordcodeDecoder<'a> {
585 pub fn new(code: &'a [u32], strings: &'a [u8], strs_base: usize) -> Self {
586 Self {
587 code,
588 strings,
589 strs_base,
590 pos: 0,
591 }
592 }
593
594 pub fn at_end(&self) -> bool {
595 self.pos >= self.code.len()
596 }
597
598 pub fn peek(&self) -> Option<u32> {
599 self.code.get(self.pos).copied()
600 }
601
602 #[allow(clippy::should_implement_trait)]
603 pub fn next(&mut self) -> Option<u32> {
604 let val = self.code.get(self.pos).copied();
605 if val.is_some() {
606 self.pos += 1;
607 }
608 val
609 }
610
611 pub fn read_string(&mut self) -> String {
612 let wc = self.next().unwrap_or(0);
613 self.decode_string(wc)
614 }
615
616 pub fn decode_string(&self, wc: u32) -> String {
617 if wc == 6 || wc == 7 {
623 return String::new();
624 }
625
626 if (wc & 2) != 0 {
627 let mut s = String::new();
629 let c1 = ((wc >> 3) & 0xff) as u8;
630 let c2 = ((wc >> 11) & 0xff) as u8;
631 let c3 = ((wc >> 19) & 0xff) as u8;
632 if c1 != 0 {
633 s.push(c1 as char);
634 }
635 if c2 != 0 {
636 s.push(c2 as char);
637 }
638 if c3 != 0 {
639 s.push(c3 as char);
640 }
641 s
642 } else {
643 let offset = (wc >> 2) as usize;
645 self.get_string_at(self.strs_base + offset)
646 }
647 }
648
649 fn get_string_at(&self, offset: usize) -> String {
650 if offset >= self.strings.len() {
651 return String::new();
652 }
653
654 let end = self.strings[offset..]
655 .iter()
656 .position(|&b| b == 0)
657 .map(|p| offset + p)
658 .unwrap_or(self.strings.len());
659
660 let raw = &self.strings[offset..end];
662 untokenize(raw)
663 }
664
665 pub fn decode(&self) -> Vec<DecodedOp> {
667 let mut decoder = WordcodeDecoder::new(self.code, self.strings, self.strs_base);
668 decoder.decode_program()
669 }
670
671 fn decode_program(&mut self) -> Vec<DecodedOp> {
672 let mut ops = Vec::new();
673
674 while let Some(wc) = self.peek() {
675 let code = wc_code(wc);
676
677 if code == WC_END {
678 self.next();
679 ops.push(DecodedOp::End);
680 break;
681 }
682
683 if let Some(op) = self.decode_next_op() {
684 ops.push(op);
685 } else {
686 break;
687 }
688 }
689
690 ops
691 }
692
693 fn decode_next_op(&mut self) -> Option<DecodedOp> {
694 let wc = self.next()?;
695 let code = wc_code(wc);
696 let data = wc_data(wc);
697
698 let op = match code {
699 WC_END => DecodedOp::End,
700 WC_LIST => self.decode_list(data),
701 WC_SUBLIST => self.decode_sublist(data),
702 WC_PIPE => self.decode_pipe(data),
703 WC_REDIR => self.decode_redir(data),
704 WC_ASSIGN => self.decode_assign(data),
705 WC_SIMPLE => self.decode_simple(data),
706 WC_TYPESET => self.decode_typeset(data),
707 WC_SUBSH => self.decode_subsh(data),
708 WC_CURSH => self.decode_cursh(data),
709 WC_TIMED => self.decode_timed(data),
710 WC_FUNCDEF => self.decode_funcdef(data),
711 WC_FOR => self.decode_for(data),
712 WC_SELECT => self.decode_select(data),
713 WC_WHILE => self.decode_while(data),
714 WC_REPEAT => self.decode_repeat(data),
715 WC_CASE => self.decode_case(data),
716 WC_IF => self.decode_if(data),
717 WC_COND => self.decode_cond(data),
718 WC_ARITH => self.decode_arith(),
719 WC_AUTOFN => DecodedOp::AutoFn,
720 WC_TRY => self.decode_try(data),
721 _ => DecodedOp::Unknown { code, data },
722 };
723
724 Some(op)
725 }
726
727 fn decode_list(&mut self, data: u32) -> DecodedOp {
728 let list_type = data & ((1 << WC_LIST_FREE) - 1);
729 let is_end = (list_type & Z_END) != 0;
730 let is_simple = (list_type & Z_SIMPLE) != 0;
731 let _skip = data >> WC_LIST_FREE;
732
733 let mut body = Vec::new();
734
735 if is_simple {
736 let lineno = self.next().unwrap_or(0);
738 body.push(DecodedOp::LineNo(lineno));
739 }
740
741 if !is_simple {
743 while let Some(wc) = self.peek() {
744 let c = wc_code(wc);
745 if c == WC_END || c == WC_LIST {
746 break;
747 }
748 if let Some(op) = self.decode_next_op() {
749 body.push(op);
750 } else {
751 break;
752 }
753 }
754 }
755
756 DecodedOp::List {
757 list_type,
758 is_end,
759 ops: body,
760 }
761 }
762
763 fn decode_sublist(&mut self, data: u32) -> DecodedOp {
764 let sublist_type = data & 3;
765 let flags = data & 0x1c;
766 let negated = (flags & WC_SUBLIST_NOT) != 0;
767 let is_simple = (flags & WC_SUBLIST_SIMPLE) != 0;
768 let _skip = data >> WC_SUBLIST_FREE;
769
770 let mut body = Vec::new();
771
772 if is_simple {
773 let lineno = self.next().unwrap_or(0);
775 body.push(DecodedOp::LineNo(lineno));
776 }
777
778 DecodedOp::Sublist {
779 sublist_type,
780 negated,
781 ops: body,
782 }
783 }
784
785 fn decode_pipe(&mut self, data: u32) -> DecodedOp {
786 let pipe_type = data & 1;
787 let lineno = data >> 1;
788 let _is_end = pipe_type == WC_PIPE_END;
789
790 DecodedOp::Pipe {
791 lineno,
792 ops: vec![],
793 }
794 }
795
796 fn decode_redir(&mut self, data: u32) -> DecodedOp {
797 let redir_type = data & 0x1f; let has_varid = (data & 0x20) != 0; let from_heredoc = (data & 0x40) != 0; let fd = self.next().unwrap_or(0) as i32;
802 let target = self.read_string();
803
804 let varid = if has_varid {
805 Some(self.read_string())
806 } else {
807 None
808 };
809
810 if from_heredoc {
811 self.next();
813 self.next();
814 }
815
816 DecodedOp::Redir {
817 redir_type,
818 fd,
819 target,
820 varid,
821 }
822 }
823
824 fn decode_assign(&mut self, data: u32) -> DecodedOp {
825 let is_array = (data & 1) != 0;
826 let num_elements = (data >> 2) as usize;
827
828 let name = self.read_string();
829
830 if is_array {
831 let mut values = Vec::with_capacity(num_elements);
832 for _ in 0..num_elements {
833 values.push(self.read_string());
834 }
835 DecodedOp::AssignArray { name, values }
836 } else {
837 let value = self.read_string();
838 DecodedOp::Assign { name, value }
839 }
840 }
841
842 fn decode_simple(&mut self, data: u32) -> DecodedOp {
843 let argc = data as usize;
844 let mut args = Vec::with_capacity(argc);
845 for _ in 0..argc {
846 args.push(self.read_string());
847 }
848 DecodedOp::Simple { args }
849 }
850
851 fn decode_typeset(&mut self, data: u32) -> DecodedOp {
852 let argc = data as usize;
853 let mut args = Vec::with_capacity(argc);
854 for _ in 0..argc {
855 args.push(self.read_string());
856 }
857
858 let num_assigns = self.next().unwrap_or(0) as usize;
860 let mut assigns = Vec::with_capacity(num_assigns);
861
862 for _ in 0..num_assigns {
863 if let Some(op) = self.decode_next_op() {
864 assigns.push(op);
865 }
866 }
867
868 DecodedOp::Typeset { args, assigns }
869 }
870
871 fn decode_subsh(&mut self, data: u32) -> DecodedOp {
872 let skip = data as usize;
873 let end_pos = self.pos + skip;
874
875 let mut body = Vec::new();
876 while self.pos < end_pos && !self.at_end() {
877 if let Some(op) = self.decode_next_op() {
878 body.push(op);
879 } else {
880 break;
881 }
882 }
883
884 DecodedOp::Subsh { ops: body }
885 }
886
887 fn decode_cursh(&mut self, data: u32) -> DecodedOp {
888 let skip = data as usize;
889 let end_pos = self.pos + skip;
890
891 let mut body = Vec::new();
892 while self.pos < end_pos && !self.at_end() {
893 if let Some(op) = self.decode_next_op() {
894 body.push(op);
895 } else {
896 break;
897 }
898 }
899
900 DecodedOp::Cursh { ops: body }
901 }
902
903 fn decode_timed(&mut self, data: u32) -> DecodedOp {
904 let timed_type = data;
905 let has_pipe = timed_type == 1; if has_pipe {
908 if let Some(op) = self.decode_next_op() {
910 return DecodedOp::Timed {
911 cmd: Some(Box::new(op)),
912 };
913 }
914 }
915
916 DecodedOp::Timed { cmd: None }
917 }
918
919 fn decode_funcdef(&mut self, data: u32) -> DecodedOp {
920 let skip = data as usize;
921
922 let num_names = self.next().unwrap_or(0) as usize;
923 let mut names = Vec::with_capacity(num_names);
924 for _ in 0..num_names {
925 names.push(self.read_string());
926 }
927
928 let _strs_offset = self.next();
930 let _strs_len = self.next();
931 let _npats = self.next();
932 let _tracing = self.next();
933
934 let _end_pos = self.pos + skip.saturating_sub(num_names + 5);
936
937 let name = names.first().cloned().unwrap_or_default();
938
939 DecodedOp::FuncDef { name, body: vec![] }
940 }
941
942 fn decode_for(&mut self, data: u32) -> DecodedOp {
943 let for_type = data & 3;
944 let _skip = data >> 2;
945
946 match for_type {
947 WC_FOR_COND => {
948 let init = self.read_string();
949 let cond = self.read_string();
950 let step = self.read_string();
951 DecodedOp::ForCond {
952 init,
953 cond,
954 step,
955 body: vec![],
956 }
957 }
958 WC_FOR_LIST => {
959 let var = self.read_string();
960 let num_words = self.next().unwrap_or(0) as usize;
961 let mut list = Vec::with_capacity(num_words);
962 for _ in 0..num_words {
963 list.push(self.read_string());
964 }
965 DecodedOp::For {
966 var,
967 list,
968 body: vec![],
969 }
970 }
971 _ => {
972 let var = self.read_string();
974 DecodedOp::For {
975 var,
976 list: vec![],
977 body: vec![],
978 }
979 }
980 }
981 }
982
983 fn decode_select(&mut self, data: u32) -> DecodedOp {
984 let select_type = data & 1;
985 let _skip = data >> 1;
986
987 let var = self.read_string();
988 let list = if select_type == 1 {
989 let num_words = self.next().unwrap_or(0) as usize;
991 let mut words = Vec::with_capacity(num_words);
992 for _ in 0..num_words {
993 words.push(self.read_string());
994 }
995 words
996 } else {
997 vec![]
998 };
999
1000 DecodedOp::Select {
1001 var,
1002 list,
1003 body: vec![],
1004 }
1005 }
1006
1007 fn decode_while(&mut self, data: u32) -> DecodedOp {
1008 let is_until = (data & 1) != 0;
1009 let _skip = data >> 1;
1010 DecodedOp::While {
1011 cond: vec![],
1012 body: vec![],
1013 is_until,
1014 }
1015 }
1016
1017 fn decode_repeat(&mut self, data: u32) -> DecodedOp {
1018 let _skip = data;
1019 let count = self.read_string();
1020 DecodedOp::Repeat {
1021 count,
1022 body: vec![],
1023 }
1024 }
1025
1026 fn decode_case(&mut self, data: u32) -> DecodedOp {
1027 let case_type = data & 7;
1028 let _skip = data >> WC_CASE_FREE;
1029
1030 if case_type == WC_CASE_HEAD {
1031 let word = self.read_string();
1032 DecodedOp::Case {
1033 word,
1034 cases: vec![],
1035 }
1036 } else {
1037 let pattern = self.read_string();
1039 let _npats = self.next();
1040 DecodedOp::CaseItem {
1041 pattern,
1042 terminator: case_type,
1043 body: vec![],
1044 }
1045 }
1046 }
1047
1048 fn decode_if(&mut self, data: u32) -> DecodedOp {
1049 let if_type = data & 3;
1050 let _skip = data >> 2;
1051
1052 DecodedOp::If {
1053 if_type,
1054 conditions: vec![],
1055 else_body: None,
1056 }
1057 }
1058
1059 fn decode_cond(&mut self, data: u32) -> DecodedOp {
1060 let cond_type = data & 127;
1061 let _skip = data >> 7;
1062
1063 let args = match cond_type {
1065 1 => vec![],
1067 2 | 3 => vec![],
1069 _ if cond_type >= 7 => {
1071 vec![self.read_string(), self.read_string()]
1072 }
1073 _ => {
1075 vec![self.read_string()]
1076 }
1077 };
1078
1079 DecodedOp::Cond { cond_type, args }
1080 }
1081
1082 fn decode_arith(&mut self) -> DecodedOp {
1083 let expr = self.read_string();
1084 DecodedOp::Arith { expr }
1085 }
1086
1087 fn decode_try(&mut self, data: u32) -> DecodedOp {
1088 let _skip = data;
1089 DecodedOp::Try {
1090 try_body: vec![],
1091 always_body: vec![],
1092 }
1093 }
1094}
1095
1096pub fn dump_zwc_info<P: AsRef<Path>>(path: P) -> io::Result<()> {
1101 let zwc = ZwcFile::load(&path)?;
1102
1103 println!("ZWC file: {:?}", path.as_ref());
1104 println!(
1105 " Magic: 0x{:08x} ({})",
1106 zwc.header.magic,
1107 if zwc.header.magic == FD_MAGIC {
1108 "native"
1109 } else {
1110 "swapped"
1111 }
1112 );
1113 println!(" Version: zsh-{}", zwc.header.version);
1114 println!(" Header length: {} words", zwc.header.header_len);
1115 println!(" Wordcode size: {} words", zwc.wordcode.len());
1116 println!(" Functions: {}", zwc.functions.len());
1117
1118 for func in &zwc.functions {
1119 println!(
1120 " {} (offset={}, len={}, npats={})",
1121 func.name, func.start, func.len, func.npats
1122 );
1123 }
1124
1125 Ok(())
1126}
1127
1128pub fn dump_zwc_function<P: AsRef<Path>>(path: P, func_name: &str) -> io::Result<()> {
1131 let zwc = ZwcFile::load(&path)?;
1132
1133 let func = zwc.get_function(func_name).ok_or_else(|| {
1134 io::Error::new(
1135 io::ErrorKind::NotFound,
1136 format!("Function '{}' not found", func_name),
1137 )
1138 })?;
1139
1140 println!("Function: {}", func.name);
1141 println!(" Offset: {} words", func.start);
1142 println!(" Length: {} words", func.len);
1143 println!(" Patterns: {}", func.npats);
1144 println!(" Strings offset: {}", func.strs_offset);
1145
1146 let header_words = zwc.header.header_len as usize;
1148 let start_idx = (func.start as usize).saturating_sub(header_words);
1149 let end_idx = start_idx + func.len as usize;
1150
1151 if start_idx < zwc.wordcode.len() {
1152 println!("\n Wordcode:");
1153 let end = end_idx.min(zwc.wordcode.len());
1154 for (i, &wc) in zwc.wordcode[start_idx..end].iter().enumerate().take(50) {
1155 let code = wc_code(wc);
1156 let data = wc_data(wc);
1157 let code_name = match code {
1158 WC_END => "END",
1159 WC_LIST => "LIST",
1160 WC_SUBLIST => "SUBLIST",
1161 WC_PIPE => "PIPE",
1162 WC_REDIR => "REDIR",
1163 WC_ASSIGN => "ASSIGN",
1164 WC_SIMPLE => "SIMPLE",
1165 WC_TYPESET => "TYPESET",
1166 WC_SUBSH => "SUBSH",
1167 WC_CURSH => "CURSH",
1168 WC_TIMED => "TIMED",
1169 WC_FUNCDEF => "FUNCDEF",
1170 WC_FOR => "FOR",
1171 WC_SELECT => "SELECT",
1172 WC_WHILE => "WHILE",
1173 WC_REPEAT => "REPEAT",
1174 WC_CASE => "CASE",
1175 WC_IF => "IF",
1176 WC_COND => "COND",
1177 WC_ARITH => "ARITH",
1178 WC_AUTOFN => "AUTOFN",
1179 WC_TRY => "TRY",
1180 _ => "???",
1181 };
1182 println!(" [{:3}] 0x{:08x} = {} (data={})", i, wc, code_name, data);
1183 }
1184 if end - start_idx > 50 {
1185 println!(" ... ({} more words)", end - start_idx - 50);
1186 }
1187 }
1188
1189 if let Some(decoded) = zwc.decode_function(func) {
1191 println!("\n Decoded ops:");
1192 for (i, op) in decoded.body.iter().enumerate().take(20) {
1193 println!(" [{:2}] {:?}", i, op);
1194 }
1195 if decoded.body.len() > 20 {
1196 println!(" ... ({} more ops)", decoded.body.len() - 20);
1197 }
1198 }
1199
1200 Ok(())
1201}
1202
1203impl DecodedOp {
1205 pub fn to_shell_command(&self) -> Option<ShellCommand> {
1206 match self {
1207 DecodedOp::Simple { args } => {
1208 if args.is_empty() {
1209 return None;
1210 }
1211 Some(ShellCommand::Simple(SimpleCommand {
1212 assignments: vec![],
1213 words: args.iter().map(|s| ShellWord::Literal(s.clone())).collect(),
1214 redirects: vec![],
1215 }))
1216 }
1217
1218 DecodedOp::Assign { name, value } => Some(ShellCommand::Simple(SimpleCommand {
1219 assignments: vec![(name.clone(), ShellWord::Literal(value.clone()), false)],
1220 words: vec![],
1221 redirects: vec![],
1222 })),
1223
1224 DecodedOp::AssignArray { name, values } => {
1225 let array_word = ShellWord::Concat(
1226 values
1227 .iter()
1228 .map(|s| ShellWord::Literal(s.clone()))
1229 .collect(),
1230 );
1231 Some(ShellCommand::Simple(SimpleCommand {
1232 assignments: vec![(name.clone(), array_word, false)],
1233 words: vec![],
1234 redirects: vec![],
1235 }))
1236 }
1237
1238 DecodedOp::List { ops, .. } => {
1239 let commands: Vec<(ShellCommand, ListOp)> = ops
1240 .iter()
1241 .filter_map(|op| op.to_shell_command())
1242 .map(|cmd| (cmd, ListOp::Semi))
1243 .collect();
1244
1245 if commands.is_empty() {
1246 None
1247 } else if commands.len() == 1 {
1248 Some(commands.into_iter().next().unwrap().0)
1249 } else {
1250 Some(ShellCommand::List(commands))
1251 }
1252 }
1253
1254 DecodedOp::Sublist { ops, negated, .. } => {
1255 let commands: Vec<ShellCommand> =
1256 ops.iter().filter_map(|op| op.to_shell_command()).collect();
1257
1258 if commands.is_empty() {
1259 None
1260 } else {
1261 Some(ShellCommand::Pipeline(commands, *negated))
1262 }
1263 }
1264
1265 DecodedOp::Pipe { ops, .. } => {
1266 let commands: Vec<ShellCommand> =
1267 ops.iter().filter_map(|op| op.to_shell_command()).collect();
1268
1269 if commands.is_empty() {
1270 None
1271 } else if commands.len() == 1 {
1272 Some(commands.into_iter().next().unwrap())
1273 } else {
1274 Some(ShellCommand::Pipeline(commands, false))
1275 }
1276 }
1277
1278 DecodedOp::Typeset { args, assigns } => {
1279 let mut words: Vec<ShellWord> =
1281 args.iter().map(|s| ShellWord::Literal(s.clone())).collect();
1282
1283 for assign in assigns {
1285 if let DecodedOp::Assign { name, value } = assign {
1286 words.push(ShellWord::Literal(format!("{}={}", name, value)));
1287 }
1288 }
1289
1290 Some(ShellCommand::Simple(SimpleCommand {
1291 assignments: vec![],
1292 words,
1293 redirects: vec![],
1294 }))
1295 }
1296
1297 DecodedOp::Subsh { ops } => {
1298 let commands: Vec<ShellCommand> =
1299 ops.iter().filter_map(|op| op.to_shell_command()).collect();
1300 Some(ShellCommand::Compound(CompoundCommand::Subshell(commands)))
1301 }
1302
1303 DecodedOp::Cursh { ops } => {
1304 let commands: Vec<ShellCommand> =
1305 ops.iter().filter_map(|op| op.to_shell_command()).collect();
1306 Some(ShellCommand::Compound(CompoundCommand::BraceGroup(
1307 commands,
1308 )))
1309 }
1310
1311 DecodedOp::For { var, list, body } => {
1312 let words = if list.is_empty() {
1313 None
1314 } else {
1315 Some(list.iter().map(|s| ShellWord::Literal(s.clone())).collect())
1316 };
1317 let body_cmds: Vec<ShellCommand> =
1318 body.iter().filter_map(|op| op.to_shell_command()).collect();
1319 Some(ShellCommand::Compound(CompoundCommand::For {
1320 var: var.clone(),
1321 words,
1322 body: body_cmds,
1323 }))
1324 }
1325
1326 DecodedOp::ForCond {
1327 init,
1328 cond,
1329 step,
1330 body,
1331 } => {
1332 let body_cmds: Vec<ShellCommand> =
1333 body.iter().filter_map(|op| op.to_shell_command()).collect();
1334 Some(ShellCommand::Compound(CompoundCommand::ForArith {
1335 init: init.clone(),
1336 cond: cond.clone(),
1337 step: step.clone(),
1338 body: body_cmds,
1339 }))
1340 }
1341
1342 DecodedOp::While {
1343 cond,
1344 body,
1345 is_until,
1346 } => {
1347 let cond_cmds: Vec<ShellCommand> =
1348 cond.iter().filter_map(|op| op.to_shell_command()).collect();
1349 let body_cmds: Vec<ShellCommand> =
1350 body.iter().filter_map(|op| op.to_shell_command()).collect();
1351
1352 if *is_until {
1353 Some(ShellCommand::Compound(CompoundCommand::Until {
1354 condition: cond_cmds,
1355 body: body_cmds,
1356 }))
1357 } else {
1358 Some(ShellCommand::Compound(CompoundCommand::While {
1359 condition: cond_cmds,
1360 body: body_cmds,
1361 }))
1362 }
1363 }
1364
1365 DecodedOp::FuncDef { name, body } => {
1366 let body_cmds: Vec<ShellCommand> =
1367 body.iter().filter_map(|op| op.to_shell_command()).collect();
1368
1369 let func_body = if body_cmds.is_empty() {
1370 ShellCommand::Simple(SimpleCommand {
1372 assignments: vec![],
1373 words: vec![ShellWord::Literal(":".to_string())],
1374 redirects: vec![],
1375 })
1376 } else if body_cmds.len() == 1 {
1377 body_cmds.into_iter().next().unwrap()
1378 } else {
1379 ShellCommand::List(body_cmds.into_iter().map(|c| (c, ListOp::Semi)).collect())
1380 };
1381
1382 Some(ShellCommand::FunctionDef(name.clone(), Box::new(func_body)))
1383 }
1384
1385 DecodedOp::Arith { expr } => {
1386 Some(ShellCommand::Compound(CompoundCommand::Arith(expr.clone())))
1387 }
1388
1389 DecodedOp::End | DecodedOp::LineNo(_) | DecodedOp::AutoFn => None,
1391
1392 DecodedOp::Redir { .. } => {
1393 None
1395 }
1396
1397 DecodedOp::If {
1398 conditions,
1399 else_body,
1400 ..
1401 } => {
1402 let cond_pairs: Vec<(Vec<ShellCommand>, Vec<ShellCommand>)> = conditions
1403 .iter()
1404 .map(|(c, b)| {
1405 (
1406 c.iter().filter_map(|op| op.to_shell_command()).collect(),
1407 b.iter().filter_map(|op| op.to_shell_command()).collect(),
1408 )
1409 })
1410 .collect();
1411 let else_part: Option<Vec<ShellCommand>> = else_body.as_ref().map(|body| {
1412 body.iter()
1413 .filter_map(|op| op.to_shell_command())
1414 .collect()
1415 });
1416 Some(ShellCommand::Compound(CompoundCommand::If {
1417 conditions: cond_pairs,
1418 else_part,
1419 }))
1420 }
1421
1422 DecodedOp::Case { word, cases } => {
1423 let mapped: Vec<(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)> = cases
1424 .iter()
1425 .map(|(pat, body)| {
1426 (
1427 vec![ShellWord::Literal(pat.clone())],
1428 body.iter().filter_map(|op| op.to_shell_command()).collect(),
1429 CaseTerminator::Break,
1430 )
1431 })
1432 .collect();
1433 Some(ShellCommand::Compound(CompoundCommand::Case {
1434 word: ShellWord::Literal(word.clone()),
1435 cases: mapped,
1436 }))
1437 }
1438
1439 DecodedOp::CaseItem { .. } => {
1440 None
1445 }
1446
1447 DecodedOp::Repeat { count, body } => {
1448 let body_cmds: Vec<ShellCommand> =
1449 body.iter().filter_map(|op| op.to_shell_command()).collect();
1450 Some(ShellCommand::Compound(CompoundCommand::Repeat {
1451 count: count.clone(),
1452 body: body_cmds,
1453 }))
1454 }
1455
1456 DecodedOp::Try {
1457 try_body,
1458 always_body,
1459 } => {
1460 let try_cmds: Vec<ShellCommand> = try_body
1461 .iter()
1462 .filter_map(|op| op.to_shell_command())
1463 .collect();
1464 let always_cmds: Vec<ShellCommand> = always_body
1465 .iter()
1466 .filter_map(|op| op.to_shell_command())
1467 .collect();
1468 Some(ShellCommand::Compound(CompoundCommand::Try {
1469 try_body: try_cmds,
1470 always_body: always_cmds,
1471 }))
1472 }
1473
1474 DecodedOp::Select { .. } => {
1475 None
1481 }
1482
1483 DecodedOp::Cond { .. } => {
1484 None
1489 }
1490
1491 DecodedOp::Timed { .. } => {
1492 None
1495 }
1496
1497 DecodedOp::Unknown { .. } => None,
1498 }
1499 }
1500}
1501
1502#[allow(dead_code)]
1504fn redir_type_to_op(redir_type: u32) -> Option<RedirectOp> {
1505 const REDIR_WRITE: u32 = 0;
1507 const REDIR_WRITENOW: u32 = 1;
1508 const REDIR_APP: u32 = 2;
1509 const REDIR_APPNOW: u32 = 3;
1510 const REDIR_ERRWRITE: u32 = 4;
1511 const REDIR_ERRWRITENOW: u32 = 5;
1512 const REDIR_ERRAPP: u32 = 6;
1513 const REDIR_ERRAPPNOW: u32 = 7;
1514 const REDIR_READWRITE: u32 = 8;
1515 const REDIR_READ: u32 = 9;
1516 const REDIR_HEREDOC: u32 = 10;
1517 const REDIR_HEREDOCDASH: u32 = 11;
1518 const REDIR_HERESTR: u32 = 12;
1519 const REDIR_MERGEIN: u32 = 13;
1520 const REDIR_MERGEOUT: u32 = 14;
1521 const REDIR_CLOSE: u32 = 15;
1522 const REDIR_INPIPE: u32 = 16;
1523 const REDIR_OUTPIPE: u32 = 17;
1524
1525 match redir_type {
1526 REDIR_WRITE | REDIR_WRITENOW => Some(RedirectOp::Write),
1527 REDIR_APP | REDIR_APPNOW => Some(RedirectOp::Append),
1528 REDIR_ERRWRITE | REDIR_ERRWRITENOW => Some(RedirectOp::WriteBoth),
1529 REDIR_ERRAPP | REDIR_ERRAPPNOW => Some(RedirectOp::AppendBoth),
1530 REDIR_READWRITE => Some(RedirectOp::ReadWrite),
1531 REDIR_READ => Some(RedirectOp::Read),
1532 REDIR_HEREDOC | REDIR_HEREDOCDASH => Some(RedirectOp::HereDoc),
1533 REDIR_HERESTR => Some(RedirectOp::HereString),
1534 REDIR_MERGEIN => Some(RedirectOp::DupRead),
1535 REDIR_MERGEOUT => Some(RedirectOp::DupWrite),
1536 REDIR_CLOSE | REDIR_INPIPE | REDIR_OUTPIPE => None, _ => None,
1538 }
1539}
1540
1541impl DecodedFunction {
1542 pub fn to_shell_function(&self) -> Option<ShellCommand> {
1544 let body_cmds: Vec<ShellCommand> = self
1545 .body
1546 .iter()
1547 .filter_map(|op| op.to_shell_command())
1548 .collect();
1549
1550 let func_body = if body_cmds.is_empty() {
1551 ShellCommand::Simple(SimpleCommand {
1552 assignments: vec![],
1553 words: vec![ShellWord::Literal(":".to_string())],
1554 redirects: vec![],
1555 })
1556 } else if body_cmds.len() == 1 {
1557 body_cmds.into_iter().next().unwrap()
1558 } else {
1559 ShellCommand::List(body_cmds.into_iter().map(|c| (c, ListOp::Semi)).collect())
1560 };
1561
1562 let name = self
1564 .name
1565 .rsplit('/')
1566 .next()
1567 .unwrap_or(&self.name)
1568 .to_string();
1569
1570 Some(ShellCommand::FunctionDef(name, Box::new(func_body)))
1571 }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 use super::*;
1577
1578 #[test]
1579 fn test_wc_code() {
1580 assert_eq!(wc_code(WC_LIST), WC_LIST);
1581 assert_eq!(wc_code(WC_SIMPLE | (5 << WC_CODEBITS)), WC_SIMPLE);
1582 }
1583
1584 #[test]
1585 fn test_wc_data() {
1586 let wc = WC_SIMPLE | (42 << WC_CODEBITS);
1587 assert_eq!(wc_data(wc), 42);
1588 }
1589
1590 #[test]
1591 fn test_load_src_zwc() {
1592 let path = "/Users/wizard/.zinit/plugins/MenkeTechnologies---zsh-more-completions/src.zwc";
1593 if !std::path::Path::new(path).exists() {
1594 eprintln!("Skipping test - {} not found", path);
1595 return;
1596 }
1597
1598 let zwc = ZwcFile::load(path).expect("Failed to load src.zwc");
1599 println!("Loaded {} functions from src.zwc", zwc.function_count());
1600
1601 assert!(
1603 zwc.function_count() > 1000,
1604 "Expected > 1000 functions, got {}",
1605 zwc.function_count()
1606 );
1607
1608 let funcs = zwc.list_functions();
1610 println!("First 10 functions: {:?}", &funcs[..10.min(funcs.len())]);
1611
1612 if let Some(func) = zwc.get_function("_ls") {
1614 println!("Found _ls function");
1615 if let Some(decoded) = zwc.decode_function(func) {
1616 println!("Decoded _ls: {} ops", decoded.body.len());
1617 }
1618 }
1619 }
1620
1621 #[test]
1622 fn test_load_zshrc_zwc() {
1623 let home = std::env::var("HOME").unwrap_or_default();
1624 let path = format!("{}/.zshrc.zwc", home);
1625 if !std::path::Path::new(&path).exists() {
1626 eprintln!("Skipping test - {} not found", path);
1627 return;
1628 }
1629
1630 let zwc = ZwcFile::load(&path).expect("Failed to load .zshrc.zwc");
1631 println!("Loaded {} functions from .zshrc.zwc", zwc.function_count());
1632
1633 for name in zwc.list_functions() {
1634 println!(" Function: {}", name);
1635 if let Some(func) = zwc.get_function(name) {
1636 if let Some(decoded) = zwc.decode_function(func) {
1637 println!(" Decoded: {} ops", decoded.body.len());
1638 for (i, op) in decoded.body.iter().take(3).enumerate() {
1639 if let Some(cmd) = op.to_shell_command() {
1640 println!(" [{}] -> ShellCommand OK", i);
1641 } else {
1642 println!(" [{}] {:?}", i, op);
1643 }
1644 }
1645 }
1646 }
1647 }
1648 }
1649
1650 #[test]
1651 fn decoded_op_if_converts_to_compound_if() {
1652 let cmd_a = DecodedOp::Simple {
1653 args: vec!["true".into()],
1654 };
1655 let cmd_b = DecodedOp::Simple {
1656 args: vec!["false".into()],
1657 };
1658 let op = DecodedOp::If {
1659 if_type: 0,
1660 conditions: vec![(vec![cmd_a], vec![cmd_b])],
1661 else_body: None,
1662 };
1663 let result = op.to_shell_command();
1664 match result {
1665 Some(ShellCommand::Compound(CompoundCommand::If {
1666 conditions,
1667 else_part,
1668 })) => {
1669 assert_eq!(conditions.len(), 1);
1670 assert!(else_part.is_none());
1671 assert_eq!(conditions[0].0.len(), 1);
1672 assert_eq!(conditions[0].1.len(), 1);
1673 }
1674 other => panic!("expected If, got {:?}", other),
1675 }
1676 }
1677
1678 #[test]
1679 fn decoded_op_repeat_converts_with_count_and_body() {
1680 let body = DecodedOp::Simple {
1681 args: vec!["echo".into(), "hi".into()],
1682 };
1683 let op = DecodedOp::Repeat {
1684 count: "3".into(),
1685 body: vec![body],
1686 };
1687 match op.to_shell_command() {
1688 Some(ShellCommand::Compound(CompoundCommand::Repeat { count, body })) => {
1689 assert_eq!(count, "3");
1690 assert_eq!(body.len(), 1);
1691 }
1692 other => panic!("expected Repeat, got {:?}", other),
1693 }
1694 }
1695
1696 #[test]
1697 fn decoded_op_case_converts_each_pattern_branch() {
1698 let body_one = DecodedOp::Simple {
1699 args: vec!["echo".into(), "one".into()],
1700 };
1701 let body_two = DecodedOp::Simple {
1702 args: vec!["echo".into(), "two".into()],
1703 };
1704 let op = DecodedOp::Case {
1705 word: "$x".into(),
1706 cases: vec![
1707 ("a*".into(), vec![body_one]),
1708 ("b*".into(), vec![body_two]),
1709 ],
1710 };
1711 match op.to_shell_command() {
1712 Some(ShellCommand::Compound(CompoundCommand::Case { cases, .. })) => {
1713 assert_eq!(cases.len(), 2);
1714 assert_eq!(cases[0].0.len(), 1);
1715 assert_eq!(cases[1].0.len(), 1);
1716 }
1717 other => panic!("expected Case, got {:?}", other),
1718 }
1719 }
1720
1721 #[test]
1722 fn decoded_op_try_converts_both_arms() {
1723 let try_arm = DecodedOp::Simple {
1724 args: vec!["false".into()],
1725 };
1726 let always_arm = DecodedOp::Simple {
1727 args: vec!["echo".into(), "done".into()],
1728 };
1729 let op = DecodedOp::Try {
1730 try_body: vec![try_arm],
1731 always_body: vec![always_arm],
1732 };
1733 match op.to_shell_command() {
1734 Some(ShellCommand::Compound(CompoundCommand::Try {
1735 try_body,
1736 always_body,
1737 })) => {
1738 assert_eq!(try_body.len(), 1);
1739 assert_eq!(always_body.len(), 1);
1740 }
1741 other => panic!("expected Try, got {:?}", other),
1742 }
1743 }
1744
1745 #[test]
1746 fn test_load_zshenv_zwc() {
1747 let home = std::env::var("HOME").unwrap_or_default();
1748 let path = format!("{}/.zshenv.zwc", home);
1749 if !std::path::Path::new(&path).exists() {
1750 eprintln!("Skipping test - {} not found", path);
1751 return;
1752 }
1753
1754 let zwc = ZwcFile::load(&path).expect("Failed to load .zshenv.zwc");
1755 println!("Loaded {} functions from .zshenv.zwc", zwc.function_count());
1756
1757 for name in zwc.list_functions() {
1758 println!(" Function: {}", name);
1759 if let Some(func) = zwc.get_function(name) {
1760 if let Some(decoded) = zwc.decode_function(func) {
1761 println!(" Decoded: {} ops", decoded.body.len());
1762 }
1763 }
1764 }
1765 }
1766}