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