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