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