Skip to main content

wpl/parser/
wpl_fun.rs

1use std::net::IpAddr;
2
3use smol_str::SmolStr;
4use winnow::{
5    Parser,
6    ascii::{digit1, multispace0},
7    combinator::{alt, fail},
8    token::literal,
9};
10use wp_primitives::{
11    WResult,
12    fun::{fun_trait::Fun0Builder, parser::call_fun_args0},
13};
14use wp_primitives::{
15    atom::take_string,
16    fun::{
17        fun_trait::{Fun1Builder, Fun2Builder, ParseNext},
18        parser::{call_fun_args1, call_fun_args2, take_arr},
19    },
20};
21
22use crate::ast::{
23    WplFun,
24    processor::{
25        CharsHas, CharsIn, CharsInArg, CharsNotHas, CharsNotHasArg, CharsValue, DigitHas,
26        DigitHasArg, DigitIn, DigitInArg, DigitRange, Has, HasArg, IpIn, IpInArg, PipeNot,
27        RegexMatch, ReplaceFunc, SelectLast, StartsWith, TakeField, TargetCharsHas, TargetCharsIn,
28        TargetCharsNotHas, TargetDigitHas, TargetDigitIn, TargetHas, TargetIpIn, normalize_target,
29    },
30};
31
32use super::utils::take_key;
33
34/// 解析带引号的字符串:"any string, with special chars"
35/// 支持转义字符:\" \\ \n \t
36fn take_quoted_string(input: &mut &str) -> WResult<String> {
37    literal("\"").parse_next(input)?;
38
39    let mut result = String::new();
40    let mut chars = input.chars();
41
42    loop {
43        match chars.next() {
44            None => {
45                return fail.parse_next(input);
46            }
47            Some('\\') => {
48                // 处理转义字符
49                match chars.next() {
50                    Some('"') => result.push('"'),
51                    Some('\\') => result.push('\\'),
52                    Some('n') => result.push('\n'),
53                    Some('t') => result.push('\t'),
54                    _ => {
55                        return fail.parse_next(input);
56                    }
57                }
58            }
59            Some('"') => {
60                // 结束引号
61                let consumed = input.len() - chars.as_str().len();
62                *input = &input[consumed..];
63                return Ok(result);
64            }
65            Some(ch) => result.push(ch),
66        }
67    }
68}
69
70/// 解析单引号字符串:'any string, with special chars'
71/// 单引号为原始字符串,只支持 \' 转义单引号本身,其他字符按字面意思处理
72fn take_single_quoted_string(input: &mut &str) -> WResult<String> {
73    literal("'").parse_next(input)?;
74
75    let mut result = String::new();
76    let mut chars = input.chars();
77
78    loop {
79        match chars.next() {
80            None => {
81                return fail.parse_next(input);
82            }
83            Some('\\') => {
84                // 单引号字符串只处理 \' 转义
85                match chars.as_str().chars().next() {
86                    Some('\'') => {
87                        result.push('\'');
88                        chars.next(); // 消费 '
89                    }
90                    _ => {
91                        // 其他情况,\ 按字面意思处理
92                        result.push('\\');
93                    }
94                }
95            }
96            Some('\'') => {
97                // 结束引号
98                let consumed = input.len() - chars.as_str().len();
99                *input = &input[consumed..];
100                return Ok(result);
101            }
102            Some(ch) => result.push(ch),
103        }
104    }
105}
106
107/// 解析字符串:支持单引号、双引号(可包含逗号、空格等特殊字符)和不带引号
108fn take_string_or_quoted(input: &mut &str) -> WResult<String> {
109    multispace0.parse_next(input)?;
110    alt((
111        take_quoted_string,
112        take_single_quoted_string,
113        take_string.map(|s: &str| s.to_string()),
114    ))
115    .parse_next(input)
116}
117
118pub fn wpl_fun(input: &mut &str) -> WResult<WplFun> {
119    multispace0.parse_next(input)?;
120    let fun = alt((
121        // Parse not() wrapper function first (needs special handling for recursive parsing)
122        parse_pipe_not,
123        alt((
124            // Put digit_range first to avoid any prefix matching issues
125            call_fun_args2::<DigitRangeArg>.map(|arg| {
126                WplFun::DigitRange(DigitRange {
127                    begin: arg.begin,
128                    end: arg.end,
129                })
130            }),
131            call_fun_args1::<RegexMatch>.map(WplFun::RegexMatch),
132            call_fun_args1::<StartsWith>.map(WplFun::StartsWith),
133            call_fun_args1::<TakeField>.map(WplFun::SelectTake),
134            call_fun_args0::<SelectLast>.map(WplFun::SelectLast),
135            call_fun_args2::<TargetCharsHas>.map(WplFun::TargetCharsHas),
136            call_fun_args1::<CharsHas>.map(WplFun::CharsHas),
137            call_fun_args2::<TargetCharsNotHas>.map(WplFun::TargetCharsNotHas),
138            call_fun_args1::<CharsNotHasArg>
139                .map(|arg| WplFun::CharsNotHas(CharsNotHas { value: arg.value })),
140            call_fun_args2::<TargetCharsIn>.map(WplFun::TargetCharsIn),
141            call_fun_args1::<CharsInArg>.map(|arg| WplFun::CharsIn(CharsIn { value: arg.value })),
142        )),
143        alt((
144            call_fun_args2::<TargetDigitHas>.map(WplFun::TargetDigitHas),
145            call_fun_args1::<DigitHasArg>
146                .map(|arg| WplFun::DigitHas(DigitHas { value: arg.value })),
147            call_fun_args2::<TargetDigitIn>.map(WplFun::TargetDigitIn),
148            call_fun_args1::<DigitInArg>.map(|arg| WplFun::DigitIn(DigitIn { value: arg.value })),
149            call_fun_args2::<TargetIpIn>.map(WplFun::TargetIpIn),
150            call_fun_args1::<IpInArg>.map(|arg| WplFun::IpIn(IpIn { value: arg.value })),
151            call_fun_args1::<TargetHas>.map(WplFun::TargetHas),
152            call_fun_args0::<HasArg>.map(|_| WplFun::Has(Has)),
153            call_fun_args0::<JsonUnescape>.map(WplFun::TransJsonUnescape),
154            call_fun_args0::<Base64Decode>.map(WplFun::TransBase64Decode),
155            call_fun_args2::<ReplaceFunc>.map(WplFun::TransCharsReplace),
156        )),
157    ))
158    .parse_next(input)?;
159    Ok(fun)
160}
161
162/// Parse not(inner_function) - requires special handling for recursive parsing
163fn parse_pipe_not(input: &mut &str) -> WResult<WplFun> {
164    // Match "not"
165    literal("not").parse_next(input)?;
166    multispace0.parse_next(input)?;
167    // Match "("
168    literal("(").parse_next(input)?;
169    multispace0.parse_next(input)?;
170    // Recursively parse inner function
171    let inner = wpl_fun.parse_next(input)?;
172    multispace0.parse_next(input)?;
173    // Match ")"
174    literal(")").parse_next(input)?;
175
176    Ok(WplFun::PipeNot(PipeNot {
177        inner: Box::new(inner),
178    }))
179}
180
181impl Fun2Builder for TargetDigitHas {
182    type ARG1 = SmolStr;
183    type ARG2 = i64;
184
185    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
186        multispace0.parse_next(data)?;
187        let val = take_key.parse_next(data)?;
188        Ok(val.into())
189    }
190    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
191        multispace0.parse_next(data)?;
192        let val = digit1.parse_next(data)?;
193        Ok(val.parse::<i64>().unwrap_or(0))
194    }
195
196    fn fun_name() -> &'static str {
197        "f_digit_has"
198    }
199
200    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
201        Self {
202            target: normalize_target(args.0),
203            value: args.1,
204        }
205    }
206}
207
208impl Fun1Builder for CharsHas {
209    type ARG1 = SmolStr;
210
211    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
212        multispace0.parse_next(data)?;
213        let val = take_string.parse_next(data)?;
214        Ok(val.into())
215    }
216
217    fn fun_name() -> &'static str {
218        "chars_has"
219    }
220
221    fn build(args: Self::ARG1) -> Self {
222        Self { value: args }
223    }
224}
225
226impl Fun1Builder for CharsNotHasArg {
227    type ARG1 = SmolStr;
228
229    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
230        multispace0.parse_next(data)?;
231        let val = take_string.parse_next(data)?;
232        Ok(val.into())
233    }
234
235    fn fun_name() -> &'static str {
236        "chars_not_has"
237    }
238
239    fn build(args: Self::ARG1) -> Self {
240        Self { value: args }
241    }
242}
243
244impl Fun1Builder for CharsInArg {
245    type ARG1 = Vec<CharsValue>;
246
247    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
248        take_arr::<CharsValue>(data)
249    }
250
251    fn fun_name() -> &'static str {
252        "chars_in"
253    }
254
255    fn build(args: Self::ARG1) -> Self {
256        let value = args.iter().map(|i| i.0.clone()).collect();
257        Self { value }
258    }
259}
260
261impl Fun1Builder for DigitHasArg {
262    type ARG1 = i64;
263
264    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
265        multispace0.parse_next(data)?;
266        let val = digit1.parse_next(data)?;
267        Ok(val.parse::<i64>().unwrap_or(0))
268    }
269
270    fn fun_name() -> &'static str {
271        "digit_has"
272    }
273
274    fn build(args: Self::ARG1) -> Self {
275        Self { value: args }
276    }
277}
278
279impl Fun1Builder for DigitInArg {
280    type ARG1 = Vec<i64>;
281
282    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
283        take_arr::<i64>(data)
284    }
285
286    fn fun_name() -> &'static str {
287        "digit_in"
288    }
289
290    fn build(args: Self::ARG1) -> Self {
291        Self { value: args }
292    }
293}
294
295impl Fun1Builder for IpInArg {
296    type ARG1 = Vec<IpAddr>;
297
298    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
299        take_arr::<IpAddr>(data)
300    }
301
302    fn fun_name() -> &'static str {
303        "ip_in"
304    }
305
306    fn build(args: Self::ARG1) -> Self {
307        Self { value: args }
308    }
309}
310
311impl Fun0Builder for HasArg {
312    fn fun_name() -> &'static str {
313        "has"
314    }
315
316    fn build() -> Self {
317        HasArg
318    }
319}
320impl Fun2Builder for TargetCharsHas {
321    type ARG1 = SmolStr;
322    type ARG2 = SmolStr;
323
324    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
325        multispace0.parse_next(data)?;
326        let val = take_key.parse_next(data)?;
327        Ok(val.into())
328    }
329    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
330        multispace0.parse_next(data)?;
331        let val = take_string.parse_next(data)?;
332        Ok(val.into())
333    }
334
335    fn fun_name() -> &'static str {
336        "f_chars_has"
337    }
338    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
339        Self {
340            target: normalize_target(args.0),
341            value: args.1,
342        }
343    }
344}
345
346impl Fun2Builder for TargetCharsNotHas {
347    type ARG1 = SmolStr;
348    type ARG2 = SmolStr;
349
350    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
351        multispace0.parse_next(data)?;
352        let val = take_key.parse_next(data)?;
353        Ok(val.into())
354    }
355    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
356        multispace0.parse_next(data)?;
357        let val = take_string.parse_next(data)?;
358        Ok(val.into())
359    }
360
361    fn fun_name() -> &'static str {
362        "f_chars_not_has"
363    }
364    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
365        Self {
366            target: normalize_target(args.0),
367            value: args.1,
368        }
369    }
370}
371
372impl ParseNext<CharsValue> for CharsValue {
373    fn parse_next(input: &mut &str) -> WResult<CharsValue> {
374        let val = take_string.parse_next(input)?;
375        Ok(CharsValue(val.into()))
376    }
377}
378impl Fun2Builder for TargetCharsIn {
379    type ARG1 = SmolStr;
380    type ARG2 = Vec<CharsValue>;
381    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
382        multispace0.parse_next(data)?;
383        let val = take_key.parse_next(data)?;
384        Ok(val.into())
385    }
386
387    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
388        take_arr::<CharsValue>(data)
389    }
390
391    fn fun_name() -> &'static str {
392        "f_chars_in"
393    }
394
395    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
396        let value: Vec<SmolStr> = args.1.iter().map(|i| i.0.clone()).collect();
397        Self {
398            target: normalize_target(args.0),
399            value,
400        }
401    }
402}
403
404impl Fun2Builder for TargetDigitIn {
405    type ARG1 = SmolStr;
406    type ARG2 = Vec<i64>;
407
408    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
409        take_arr::<i64>(data)
410    }
411    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
412        multispace0.parse_next(data)?;
413        let val = take_key.parse_next(data)?;
414        Ok(val.into())
415    }
416
417    fn fun_name() -> &'static str {
418        "f_digit_in"
419    }
420    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
421        Self {
422            target: normalize_target(args.0),
423            value: args.1,
424        }
425    }
426}
427impl Fun1Builder for TargetHas {
428    type ARG1 = SmolStr;
429
430    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
431        multispace0.parse_next(data)?;
432        let val = take_key.parse_next(data)?;
433        Ok(val.into())
434    }
435
436    fn fun_name() -> &'static str {
437        "f_has"
438    }
439
440    fn build(args: Self::ARG1) -> Self {
441        Self {
442            target: normalize_target(args),
443        }
444    }
445}
446
447impl Fun2Builder for TargetIpIn {
448    type ARG1 = SmolStr;
449    type ARG2 = Vec<IpAddr>;
450
451    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
452        take_arr::<IpAddr>(data)
453    }
454    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
455        multispace0.parse_next(data)?;
456        let val = take_key.parse_next(data)?;
457        Ok(val.into())
458    }
459
460    fn fun_name() -> &'static str {
461        "f_ip_in"
462    }
463    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
464        Self {
465            target: normalize_target(args.0),
466            value: args.1,
467        }
468    }
469}
470
471// ---------------- String Mode ----------------
472use crate::ast::processor::JsonUnescape;
473
474impl Fun0Builder for JsonUnescape {
475    fn fun_name() -> &'static str {
476        "json_unescape"
477    }
478
479    fn build() -> Self {
480        JsonUnescape {}
481    }
482}
483
484use crate::ast::processor::Base64Decode;
485impl Fun0Builder for Base64Decode {
486    fn fun_name() -> &'static str {
487        "base64_decode"
488    }
489
490    fn build() -> Self {
491        Base64Decode {}
492    }
493}
494
495impl Fun2Builder for ReplaceFunc {
496    type ARG1 = SmolStr;
497    type ARG2 = SmolStr;
498
499    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
500        multispace0.parse_next(data)?;
501        let val = take_string_or_quoted.parse_next(data)?;
502        Ok(val.into())
503    }
504
505    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
506        multispace0.parse_next(data)?;
507        let val = take_string_or_quoted.parse_next(data)?;
508        Ok(val.into())
509    }
510
511    fn fun_name() -> &'static str {
512        "chars_replace"
513    }
514
515    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
516        Self {
517            target: args.0,
518            value: args.1,
519        }
520    }
521}
522
523/// Parser argument for `digit_range(begin, end)` - converted to DigitRange
524#[derive(Clone, Debug, PartialEq)]
525pub struct DigitRangeArg {
526    pub(crate) begin: i64,
527    pub(crate) end: i64,
528}
529
530impl Fun2Builder for DigitRangeArg {
531    type ARG1 = i64;
532    type ARG2 = i64;
533
534    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
535        multispace0.parse_next(data)?;
536        let val = digit1.parse_next(data)?;
537        Ok(val.parse::<i64>().unwrap_or(0))
538    }
539
540    fn args2(data: &mut &str) -> WResult<Self::ARG2> {
541        multispace0.parse_next(data)?;
542        let val = digit1.parse_next(data)?;
543        Ok(val.parse::<i64>().unwrap_or(0))
544    }
545
546    fn fun_name() -> &'static str {
547        "digit_range"
548    }
549
550    fn build(args: (Self::ARG1, Self::ARG2)) -> Self {
551        Self {
552            begin: args.0,
553            end: args.1,
554        }
555    }
556}
557
558impl Fun1Builder for RegexMatch {
559    type ARG1 = SmolStr;
560
561    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
562        multispace0.parse_next(data)?;
563        let val = take_string_or_quoted.parse_next(data)?;
564        Ok(val.into())
565    }
566
567    fn fun_name() -> &'static str {
568        "regex_match"
569    }
570
571    fn build(args: Self::ARG1) -> Self {
572        Self { pattern: args }
573    }
574}
575
576impl Fun1Builder for StartsWith {
577    type ARG1 = SmolStr;
578
579    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
580        multispace0.parse_next(data)?;
581        let val = take_string_or_quoted.parse_next(data)?;
582        Ok(val.into())
583    }
584
585    fn fun_name() -> &'static str {
586        "starts_with"
587    }
588
589    fn build(args: Self::ARG1) -> Self {
590        Self { prefix: args }
591    }
592}
593
594impl Fun1Builder for TakeField {
595    type ARG1 = SmolStr;
596
597    fn args1(data: &mut &str) -> WResult<Self::ARG1> {
598        multispace0.parse_next(data)?;
599        let val = alt((
600            take_quoted_string.map(SmolStr::from),
601            take_single_quoted_string.map(SmolStr::from),
602            take_key.map(SmolStr::from),
603        ))
604        .parse_next(data)?;
605        Ok(val)
606    }
607
608    fn fun_name() -> &'static str {
609        "take"
610    }
611
612    fn build(args: Self::ARG1) -> Self {
613        Self { target: args }
614    }
615}
616
617impl Fun0Builder for SelectLast {
618    fn fun_name() -> &'static str {
619        "last"
620    }
621
622    fn build() -> Self {
623        SelectLast {}
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use std::net::{Ipv4Addr, Ipv6Addr};
630
631    use orion_error::TestAssert;
632
633    use crate::ast::processor::{Has, JsonUnescape, ReplaceFunc, SelectLast, TakeField};
634
635    use super::*;
636
637    #[test]
638    fn test_parse_fun() {
639        let fun = wpl_fun.parse(r#"f_has(src)"#).assert();
640        assert_eq!(
641            fun,
642            WplFun::TargetHas(TargetHas {
643                target: Some("src".into())
644            })
645        );
646
647        let fun = wpl_fun.parse("has()").assert();
648        assert_eq!(fun, WplFun::Has(Has));
649
650        let fun = wpl_fun.parse(r#"f_digit_in(src, [1,2,3])"#).assert();
651        assert_eq!(
652            fun,
653            WplFun::TargetDigitIn(TargetDigitIn {
654                target: Some("src".into()),
655                value: vec![1, 2, 3]
656            })
657        );
658
659        let fun = wpl_fun.parse("digit_has(42)").assert();
660        assert_eq!(fun, WplFun::DigitHas(DigitHas { value: 42 }));
661
662        let fun = wpl_fun.parse("digit_in([4,5])").assert();
663        assert_eq!(fun, WplFun::DigitIn(DigitIn { value: vec![4, 5] }));
664
665        let fun = wpl_fun
666            .parse(r#"f_ip_in(src, [127.0.0.1, 127.0.0.2])"#)
667            .assert();
668        assert_eq!(
669            fun,
670            WplFun::TargetIpIn(TargetIpIn {
671                target: Some("src".into()),
672                value: vec![
673                    IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
674                    IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2))
675                ]
676            })
677        );
678
679        let fun = wpl_fun.parse(r#"ip_in([127.0.0.1,::1])"#).assert();
680        assert_eq!(
681            fun,
682            WplFun::IpIn(IpIn {
683                value: vec![
684                    IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
685                    IpAddr::V6(Ipv6Addr::LOCALHOST),
686                ],
687            })
688        );
689
690        // IPv6 裸字面量与混合示例
691        let fun = wpl_fun
692            .parse(r#"f_ip_in(src, [::1, 2001:db8::1])"#)
693            .assert();
694        assert_eq!(
695            fun,
696            WplFun::TargetIpIn(TargetIpIn {
697                target: Some("src".into()),
698                value: vec![
699                    IpAddr::V6(Ipv6Addr::LOCALHOST),
700                    IpAddr::V6("2001:db8::1".parse().unwrap()),
701                ]
702            })
703        );
704
705        let fun = wpl_fun.parse("json_unescape()").assert();
706        assert_eq!(fun, WplFun::TransJsonUnescape(JsonUnescape {}));
707
708        assert!(wpl_fun.parse("json_unescape(decoded)").is_err());
709
710        let fun = wpl_fun.parse("take(src)").assert();
711        assert_eq!(
712            fun,
713            WplFun::SelectTake(TakeField {
714                target: "src".into(),
715            })
716        );
717
718        // Test take with double quotes
719        let fun = wpl_fun.parse(r#"take("@key")"#).assert();
720        assert_eq!(
721            fun,
722            WplFun::SelectTake(TakeField {
723                target: "@key".into(),
724            })
725        );
726
727        // Test take with single quotes
728        let fun = wpl_fun.parse("take('@field')").assert();
729        assert_eq!(
730            fun,
731            WplFun::SelectTake(TakeField {
732                target: "@field".into(),
733            })
734        );
735
736        // Test take with special characters in double quotes
737        let fun = wpl_fun.parse(r#"take("field with spaces")"#).assert();
738        assert_eq!(
739            fun,
740            WplFun::SelectTake(TakeField {
741                target: "field with spaces".into(),
742            })
743        );
744
745        // Test take with special characters in single quotes
746        let fun = wpl_fun.parse("take('field,with,commas')").assert();
747        assert_eq!(
748            fun,
749            WplFun::SelectTake(TakeField {
750                target: "field,with,commas".into(),
751            })
752        );
753
754        // Test take with escaped quote in double quotes
755        let fun = wpl_fun.parse(r#"take("field\"name")"#).assert();
756        assert_eq!(
757            fun,
758            WplFun::SelectTake(TakeField {
759                target: "field\"name".into(),
760            })
761        );
762
763        // Test take with escaped quote in single quotes
764        let fun = wpl_fun.parse("take('field\\'name')").assert();
765        assert_eq!(
766            fun,
767            WplFun::SelectTake(TakeField {
768                target: "field'name".into(),
769            })
770        );
771
772        // Test single quotes are raw strings - no escape for \n, \t, etc
773        let fun = wpl_fun.parse(r"take('raw\nstring')").assert();
774        assert_eq!(
775            fun,
776            WplFun::SelectTake(TakeField {
777                target: r"raw\nstring".into(),
778            })
779        );
780
781        let fun = wpl_fun.parse(r"take('path\to\file')").assert();
782        assert_eq!(
783            fun,
784            WplFun::SelectTake(TakeField {
785                target: r"path\to\file".into(),
786            })
787        );
788
789        // Test double quotes still support escapes
790        let fun = wpl_fun.parse(r#"take("line\nbreak")"#).assert();
791        assert_eq!(
792            fun,
793            WplFun::SelectTake(TakeField {
794                target: "line\nbreak".into(),
795            })
796        );
797
798        let fun = wpl_fun.parse("last()").assert();
799        assert_eq!(fun, WplFun::SelectLast(SelectLast {}));
800
801        let fun = wpl_fun.parse("f_chars_has(_, foo)").assert();
802        assert_eq!(
803            fun,
804            WplFun::TargetCharsHas(TargetCharsHas {
805                target: None,
806                value: "foo".into(),
807            })
808        );
809
810        let fun = wpl_fun.parse("chars_has(bar)").assert();
811        assert_eq!(
812            fun,
813            WplFun::CharsHas(CharsHas {
814                value: "bar".into(),
815            })
816        );
817
818        let fun = wpl_fun.parse("chars_has(中文)").assert();
819        assert_eq!(
820            fun,
821            WplFun::CharsHas(CharsHas {
822                value: "中文".into(),
823            })
824        );
825
826        let fun = wpl_fun.parse("chars_not_has(baz)").assert();
827        assert_eq!(
828            fun,
829            WplFun::CharsNotHas(CharsNotHas {
830                value: "baz".into(),
831            })
832        );
833
834        let fun = wpl_fun.parse("chars_in([foo,bar])").assert();
835        assert_eq!(
836            fun,
837            WplFun::CharsIn(CharsIn {
838                value: vec!["foo".into(), "bar".into()],
839            })
840        );
841
842        let fun = wpl_fun.parse("base64_decode()").assert();
843        assert_eq!(fun, WplFun::TransBase64Decode(Base64Decode {}));
844        assert!(wpl_fun.parse("base64_decode(decoded)").is_err());
845
846        // chars_replace tests
847        let fun = wpl_fun.parse(r#"chars_replace(hello, hi)"#).assert();
848        assert_eq!(
849            fun,
850            WplFun::TransCharsReplace(ReplaceFunc {
851                target: "hello".into(),
852                value: "hi".into(),
853            })
854        );
855
856        let fun = wpl_fun
857            .parse(r#"chars_replace(old_value, new_value)"#)
858            .assert();
859        assert_eq!(
860            fun,
861            WplFun::TransCharsReplace(ReplaceFunc {
862                target: "old_value".into(),
863                value: "new_value".into(),
864            })
865        );
866
867        // chars_replace with Chinese characters
868        let fun = wpl_fun.parse(r#"chars_replace(旧值, 新值)"#).assert();
869        assert_eq!(
870            fun,
871            WplFun::TransCharsReplace(ReplaceFunc {
872                target: "旧值".into(),
873                value: "新值".into(),
874            })
875        );
876
877        // chars_replace with special characters
878        let fun = wpl_fun
879            .parse(r#"chars_replace(test-old, test-new)"#)
880            .assert();
881        assert_eq!(
882            fun,
883            WplFun::TransCharsReplace(ReplaceFunc {
884                target: "test-old".into(),
885                value: "test-new".into(),
886            })
887        );
888
889        // chars_replace with underscores
890        let fun = wpl_fun
891            .parse(r#"chars_replace(error_code, status_code)"#)
892            .assert();
893        assert_eq!(
894            fun,
895            WplFun::TransCharsReplace(ReplaceFunc {
896                target: "error_code".into(),
897                value: "status_code".into(),
898            })
899        );
900
901        // chars_replace with quoted strings (supports commas and spaces)
902        let fun = wpl_fun
903            .parse(r#"chars_replace("test,old", "test,new")"#)
904            .assert();
905        assert_eq!(
906            fun,
907            WplFun::TransCharsReplace(ReplaceFunc {
908                target: "test,old".into(),
909                value: "test,new".into(),
910            })
911        );
912
913        // chars_replace with quoted strings containing spaces
914        let fun = wpl_fun
915            .parse(r#"chars_replace("hello world", "goodbye world")"#)
916            .assert();
917        assert_eq!(
918            fun,
919            WplFun::TransCharsReplace(ReplaceFunc {
920                target: "hello world".into(),
921                value: "goodbye world".into(),
922            })
923        );
924
925        // chars_replace mixing quoted and unquoted
926        let fun = wpl_fun
927            .parse(r#"chars_replace("test,old", new_value)"#)
928            .assert();
929        assert_eq!(
930            fun,
931            WplFun::TransCharsReplace(ReplaceFunc {
932                target: "test,old".into(),
933                value: "new_value".into(),
934            })
935        );
936
937        // chars_replace with empty quoted string
938        let fun = wpl_fun.parse(r#"chars_replace(test, "")"#).assert();
939        assert_eq!(
940            fun,
941            WplFun::TransCharsReplace(ReplaceFunc {
942                target: "test".into(),
943                value: "".into(),
944            })
945        );
946
947        // chars_replace with escaped quotes
948        let fun = wpl_fun
949            .parse(r#"chars_replace("test,old", "\"test,new\"")"#)
950            .assert();
951        assert_eq!(
952            fun,
953            WplFun::TransCharsReplace(ReplaceFunc {
954                target: "test,old".into(),
955                value: "\"test,new\"".into(),
956            })
957        );
958
959        // chars_replace with escaped backslash
960        let fun = wpl_fun
961            .parse(r#"chars_replace("path\\to\\file", "new\\path")"#)
962            .assert();
963        assert_eq!(
964            fun,
965            WplFun::TransCharsReplace(ReplaceFunc {
966                target: "path\\to\\file".into(),
967                value: "new\\path".into(),
968            })
969        );
970
971        // chars_replace with newline and tab
972        let fun = wpl_fun
973            .parse(r#"chars_replace("line1\nline2", "tab\there")"#)
974            .assert();
975        assert_eq!(
976            fun,
977            WplFun::TransCharsReplace(ReplaceFunc {
978                target: "line1\nline2".into(),
979                value: "tab\there".into(),
980            })
981        );
982    }
983
984    #[test]
985    fn test_parse_digit_range() {
986        use winnow::Parser;
987        use wp_primitives::fun::parser::call_fun_args2;
988
989        // Direct test of DigitRangeArg parser - simple case
990        let mut input = "digit_range(1, 10)";
991        let result = call_fun_args2::<DigitRangeArg>.parse_next(&mut input);
992        assert!(
993            result.is_ok(),
994            "Simple case should parse successfully: {:?}",
995            result
996        );
997        let arg = result.unwrap();
998        assert_eq!(arg.begin, 1);
999        assert_eq!(arg.end, 10);
1000
1001        // Direct test with different values
1002        let mut input2 = "digit_range(100, 200)";
1003        let result2 = call_fun_args2::<DigitRangeArg>.parse_next(&mut input2);
1004        assert!(
1005            result2.is_ok(),
1006            "Different values should parse: {:?}",
1007            result2
1008        );
1009        let arg2 = result2.unwrap();
1010        assert_eq!(arg2.begin, 100);
1011        assert_eq!(arg2.end, 200);
1012    }
1013
1014    #[test]
1015    fn test_parse_regex_match() {
1016        let mut wpl_fun = wpl_fun;
1017
1018        // regex_match with simple pattern (use single quotes for raw string)
1019        let fun = wpl_fun.parse(r"regex_match('^\d+$')").assert();
1020        assert_eq!(
1021            fun,
1022            WplFun::RegexMatch(RegexMatch {
1023                pattern: r"^\d+$".into(),
1024            })
1025        );
1026
1027        // regex_match with complex pattern
1028        let fun = wpl_fun.parse(r"regex_match('^\w+@\w+\.\w+$')").assert();
1029        assert_eq!(
1030            fun,
1031            WplFun::RegexMatch(RegexMatch {
1032                pattern: r"^\w+@\w+\.\w+$".into(),
1033            })
1034        );
1035
1036        // regex_match with alternation
1037        let fun = wpl_fun.parse(r"regex_match('^(GET|POST|PUT)$')").assert();
1038        assert_eq!(
1039            fun,
1040            WplFun::RegexMatch(RegexMatch {
1041                pattern: r"^(GET|POST|PUT)$".into(),
1042            })
1043        );
1044    }
1045
1046    #[test]
1047    fn test_parse_start_with() {
1048        let mut wpl_fun = wpl_fun;
1049
1050        // starts_with with simple prefix
1051        let fun = wpl_fun.parse(r"starts_with('http')").assert();
1052        assert_eq!(
1053            fun,
1054            WplFun::StartsWith(StartsWith {
1055                prefix: "http".into(),
1056            })
1057        );
1058
1059        // starts_with with complex prefix
1060        let fun = wpl_fun.parse(r"starts_with('https://')").assert();
1061        assert_eq!(
1062            fun,
1063            WplFun::StartsWith(StartsWith {
1064                prefix: "https://".into(),
1065            })
1066        );
1067
1068        // starts_with with single character
1069        let fun = wpl_fun.parse(r"starts_with('/')").assert();
1070        assert_eq!(fun, WplFun::StartsWith(StartsWith { prefix: "/".into() }));
1071    }
1072
1073    #[test]
1074    fn test_parse_pipe_not() {
1075        // Test: not(f_chars_has(dev_type, NDS))
1076        let fun = wpl_fun.parse(r"not(f_chars_has(dev_type, NDS))").assert();
1077
1078        if let WplFun::PipeNot(pipe_not) = fun {
1079            if let WplFun::TargetCharsHas(inner) = *pipe_not.inner {
1080                assert_eq!(inner.target, Some("dev_type".into()));
1081                assert_eq!(inner.value.as_str(), "NDS");
1082            } else {
1083                panic!("Inner function should be TargetCharsHas");
1084            }
1085        } else {
1086            panic!("Should parse as PipeNot");
1087        }
1088
1089        // Test: not(has())
1090        let fun = wpl_fun.parse(r"not(has())").assert();
1091        if let WplFun::PipeNot(pipe_not) = fun {
1092            assert!(matches!(*pipe_not.inner, WplFun::Has(_)));
1093        } else {
1094            panic!("Should parse as PipeNot with Has");
1095        }
1096
1097        // Test: not(f_has(field_name))
1098        let fun = wpl_fun.parse(r"not(f_has(field_name))").assert();
1099        if let WplFun::PipeNot(pipe_not) = fun {
1100            if let WplFun::TargetHas(inner) = *pipe_not.inner {
1101                assert_eq!(inner.target, Some("field_name".into()));
1102            } else {
1103                panic!("Inner function should be TargetHas");
1104            }
1105        } else {
1106            panic!("Should parse as PipeNot");
1107        }
1108
1109        // Test: Double negation not(not(has()))
1110        let fun = wpl_fun.parse(r"not(not(has()))").assert();
1111        if let WplFun::PipeNot(outer_not) = fun {
1112            if let WplFun::PipeNot(inner_not) = *outer_not.inner {
1113                assert!(matches!(*inner_not.inner, WplFun::Has(_)));
1114            } else {
1115                panic!("Inner should be PipeNot");
1116            }
1117        } else {
1118            panic!("Should parse as nested PipeNot");
1119        }
1120    }
1121}