1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2025 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use crate::ast::{CertificateAttributeName, Query, QueryValue, RegexValue, SourceInfo};
19
use crate::combinator::{choice, ParseError as ParseErrorTrait};
20
use crate::parser::cookiepath::cookiepath;
21
use crate::parser::primitives::{literal, one_or_more_spaces, regex, try_literal};
22
use crate::parser::string::{quoted_oneline_string, quoted_template};
23
use crate::parser::{ParseError, ParseErrorKind, ParseResult};
24
use crate::reader::{Pos, Reader};
25

            
26
32230
pub fn query(reader: &mut Reader) -> ParseResult<Query> {
27
32230
    let start = reader.cursor();
28
32230
    let value = query_value(reader)?;
29
24620
    let end = reader.cursor();
30
24620
    Ok(Query {
31
24620
        source_info: SourceInfo::new(start.pos, end.pos),
32
24620
        value,
33
24620
    })
34
}
35

            
36
32230
fn query_value(reader: &mut Reader) -> ParseResult<QueryValue> {
37
32230
    choice(
38
32230
        &[
39
32230
            status_query,
40
32230
            version_query,
41
32230
            url_query,
42
32230
            header_query,
43
32230
            cookie_query,
44
32230
            body_query,
45
32230
            xpath_query,
46
32230
            jsonpath_query,
47
32230
            regex_query,
48
32230
            variable_query,
49
32230
            duration_query,
50
32230
            bytes_query,
51
32230
            sha256_query,
52
32230
            md5_query,
53
32230
            certificate_query,
54
32230
            ip_query,
55
32230
            redirects_query,
56
32230
        ],
57
32230
        reader,
58
    )
59
}
60

            
61
32230
fn status_query(reader: &mut Reader) -> ParseResult<QueryValue> {
62
32230
    try_literal("status", reader)?;
63
110
    Ok(QueryValue::Status)
64
}
65

            
66
32120
fn version_query(reader: &mut Reader) -> ParseResult<QueryValue> {
67
32120
    try_literal("version", reader)?;
68
100
    Ok(QueryValue::Version)
69
}
70

            
71
32020
fn url_query(reader: &mut Reader) -> ParseResult<QueryValue> {
72
32020
    try_literal("url", reader)?;
73
495
    Ok(QueryValue::Url)
74
}
75

            
76
31525
fn header_query(reader: &mut Reader) -> ParseResult<QueryValue> {
77
31525
    try_literal("header", reader)?;
78
1565
    let space0 = one_or_more_spaces(reader)?;
79
1565
    let name = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
80
1565
    Ok(QueryValue::Header { space0, name })
81
}
82

            
83
29960
fn cookie_query(reader: &mut Reader) -> ParseResult<QueryValue> {
84
29960
    try_literal("cookie", reader)?;
85
695
    let space0 = one_or_more_spaces(reader)?;
86

            
87
    // Read the whole value of the coookie path and parse it with a specialized reader.
88
695
    let start = reader.cursor();
89
695
    let s = quoted_oneline_string(reader)?;
90
    // todo should work with an encodedString in order to support escape sequence
91
    // or decode escape sequence with the cookiepath parser
92

            
93
    // We will parse the cookiepath value without `"`.
94
690
    let pos = Pos::new(start.pos.line, start.pos.column + 1);
95
690
    let mut cookiepath_reader = Reader::with_pos(s.as_str(), pos);
96
690
    let expr = cookiepath(&mut cookiepath_reader)?;
97

            
98
685
    Ok(QueryValue::Cookie { space0, expr })
99
}
100

            
101
29265
fn body_query(reader: &mut Reader) -> ParseResult<QueryValue> {
102
29265
    try_literal("body", reader)?;
103
6200
    Ok(QueryValue::Body)
104
}
105

            
106
23065
fn xpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
107
23065
    try_literal("xpath", reader)?;
108
1430
    let space0 = one_or_more_spaces(reader)?;
109
1431
    let expr = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
110
1425
    Ok(QueryValue::Xpath { space0, expr })
111
}
112

            
113
21635
fn jsonpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
114
21635
    try_literal("jsonpath", reader)?;
115
9850
    let space0 = one_or_more_spaces(reader)?;
116
    //let expr = jsonpath_expr(reader)?;
117
    //  let start = reader.state.pos.clone();
118
9851
    let expr = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
119
    //    let end = reader.state.pos.clone();
120
    //    let expr = Template {
121
    //        elements: template.elements.iter().map(|e| match e {
122
    //            TemplateElement::String { value, encoded } => HurlTemplateElement::Literal {
123
    //                value: HurlString2 { value: value.clone(), encoded: Some(encoded.clone()) }
124
    //            },
125
    //            TemplateElement::Expression(value) => HurlTemplateElement::Expression { value: value.clone() }
126
    //        }).collect(),
127
    //        quotes: true,
128
    //        source_info: SourceInfo { start, end },
129
    //    };
130

            
131
9845
    Ok(QueryValue::Jsonpath { space0, expr })
132
}
133

            
134
11785
fn regex_query(reader: &mut Reader) -> ParseResult<QueryValue> {
135
11785
    try_literal("regex", reader)?;
136
165
    let space0 = one_or_more_spaces(reader)?;
137
165
    let value = regex_value(reader)?;
138
160
    Ok(QueryValue::Regex { space0, value })
139
}
140

            
141
485
pub fn regex_value(reader: &mut Reader) -> ParseResult<RegexValue> {
142
485
    choice(
143
        &[
144
485
            |p1| match quoted_template(p1) {
145
175
                Ok(value) => Ok(RegexValue::Template(value)),
146
310
                Err(e) => Err(e),
147
485
            },
148
310
            |p1| match regex(p1) {
149
305
                Ok(value) => Ok(RegexValue::Regex(value)),
150
5
                Err(e) => Err(e),
151
310
            },
152
        ],
153
485
        reader,
154
    )
155
486
    .map_err(|e| {
156
5
        let kind = ParseErrorKind::Expecting {
157
5
            value: "\" or /".to_string(),
158
5
        };
159
5
        ParseError::new(e.pos, false, kind)
160
5
    })
161
}
162

            
163
11620
fn variable_query(reader: &mut Reader) -> ParseResult<QueryValue> {
164
11620
    try_literal("variable", reader)?;
165
1170
    let space0 = one_or_more_spaces(reader)?;
166
1170
    let name = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
167
1170
    Ok(QueryValue::Variable { space0, name })
168
}
169

            
170
10450
fn duration_query(reader: &mut Reader) -> ParseResult<QueryValue> {
171
10450
    try_literal("duration", reader)?;
172
85
    Ok(QueryValue::Duration)
173
}
174

            
175
10365
fn bytes_query(reader: &mut Reader) -> ParseResult<QueryValue> {
176
10365
    try_literal("bytes", reader)?;
177
1005
    Ok(QueryValue::Bytes)
178
}
179

            
180
9360
fn sha256_query(reader: &mut Reader) -> ParseResult<QueryValue> {
181
9360
    try_literal("sha256", reader)?;
182
265
    Ok(QueryValue::Sha256)
183
}
184

            
185
9095
fn md5_query(reader: &mut Reader) -> ParseResult<QueryValue> {
186
9095
    try_literal("md5", reader)?;
187
235
    Ok(QueryValue::Md5)
188
}
189

            
190
8860
fn certificate_query(reader: &mut Reader) -> ParseResult<QueryValue> {
191
8860
    try_literal("certificate", reader)?;
192
360
    let space0 = one_or_more_spaces(reader)?;
193
360
    let field = certificate_field(reader)?;
194
360
    Ok(QueryValue::Certificate {
195
360
        space0,
196
360
        attribute_name: field,
197
360
    })
198
}
199

            
200
8500
fn ip_query(reader: &mut Reader) -> ParseResult<QueryValue> {
201
8500
    try_literal("ip", reader)?;
202
90
    Ok(QueryValue::Ip)
203
}
204

            
205
8410
fn redirects_query(reader: &mut Reader) -> ParseResult<QueryValue> {
206
8410
    try_literal("redirects", reader)?;
207
825
    Ok(QueryValue::Redirects)
208
}
209

            
210
360
fn certificate_field(reader: &mut Reader) -> ParseResult<CertificateAttributeName> {
211
360
    literal("\"", reader)?;
212
360
    if try_literal(r#"Subject""#, reader).is_ok() {
213
60
        Ok(CertificateAttributeName::Subject)
214
300
    } else if try_literal(r#"Issuer""#, reader).is_ok() {
215
40
        Ok(CertificateAttributeName::Issuer)
216
260
    } else if try_literal(r#"Start-Date""#, reader).is_ok() {
217
90
        Ok(CertificateAttributeName::StartDate)
218
170
    } else if try_literal(r#"Expire-Date""#, reader).is_ok() {
219
130
        Ok(CertificateAttributeName::ExpireDate)
220
40
    } else if try_literal(r#"Serial-Number""#, reader).is_ok() {
221
40
        Ok(CertificateAttributeName::SerialNumber)
222
    } else if try_literal(r#"Subject-Alt-Name""#, reader).is_ok() {
223
        Ok(CertificateAttributeName::SubjectAltName)
224
    } else {
225
        let value =
226
            "Field <Subject>, <Issuer>, <Start-Date>, <Expire-Date>, <Serial-Number>, or <Subject-Alt-Name>".to_string();
227
        let kind = ParseErrorKind::Expecting { value };
228
        let cur = reader.cursor();
229
        Err(ParseError::new(cur.pos, false, kind))
230
    }
231
}
232

            
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236
    use crate::ast::{
237
        CookieAttribute, CookieAttributeName, CookiePath, Filter, FilterValue, Template,
238
        TemplateElement, Whitespace,
239
    };
240
    use crate::parser::filter::filters;
241
    use crate::reader::{CharPos, Pos};
242
    use crate::types::ToSource;
243

            
244
    #[test]
245
    fn test_query() {
246
        let mut reader = Reader::new("status");
247
        assert_eq!(
248
            query(&mut reader).unwrap(),
249
            Query {
250
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
251
                value: QueryValue::Status,
252
            }
253
        );
254
    }
255

            
256
    #[test]
257
    fn test_status_query() {
258
        let mut reader = Reader::new("status");
259
        assert_eq!(
260
            query(&mut reader).unwrap(),
261
            Query {
262
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
263
                value: QueryValue::Status,
264
            }
265
        );
266
    }
267

            
268
    #[test]
269
    fn test_header_query() {
270
        let mut reader = Reader::new("header \"Foo\"");
271
        assert_eq!(
272
            header_query(&mut reader).unwrap(),
273
            QueryValue::Header {
274
                space0: Whitespace {
275
                    value: String::from(" "),
276
                    source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 8)),
277
                },
278
                name: Template::new(
279
                    Some('"'),
280
                    vec![TemplateElement::String {
281
                        value: "Foo".to_string(),
282
                        source: "Foo".to_source(),
283
                    }],
284
                    SourceInfo::new(Pos::new(1, 8), Pos::new(1, 13))
285
                )
286
            }
287
        );
288
    }
289

            
290
    #[test]
291
    fn test_cookie_query() {
292
        let mut reader = Reader::new("cookie \"Foo[Domain]\"");
293
        assert_eq!(
294
            cookie_query(&mut reader).unwrap(),
295
            QueryValue::Cookie {
296
                space0: Whitespace {
297
                    value: String::from(" "),
298
                    source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 8)),
299
                },
300
                expr: CookiePath {
301
                    name: Template::new(
302
                        None,
303
                        vec![TemplateElement::String {
304
                            value: "Foo".to_string(),
305
                            source: "Foo".to_source(),
306
                        }],
307
                        SourceInfo::new(Pos::new(1, 9), Pos::new(1, 12))
308
                    ),
309
                    attribute: Some(CookieAttribute {
310
                        space0: Whitespace {
311
                            value: String::new(),
312
                            source_info: SourceInfo::new(Pos::new(1, 13), Pos::new(1, 13)),
313
                        },
314
                        name: CookieAttributeName::Domain("Domain".to_string()),
315
                        space1: Whitespace {
316
                            value: String::new(),
317
                            source_info: SourceInfo::new(Pos::new(1, 19), Pos::new(1, 19)),
318
                        },
319
                    }),
320
                },
321
            }
322
        );
323
        assert_eq!(reader.cursor().index, CharPos(20));
324

            
325
        // todo test with escape sequence
326
        //let mut reader = Reader::init("cookie \"cookie\u{31}\"");
327
    }
328

            
329
    #[test]
330
    fn test_xpath_query() {
331
        let mut reader = Reader::new("xpath \"normalize-space(//head/title)\"");
332
        assert_eq!(
333
            xpath_query(&mut reader).unwrap(),
334
            QueryValue::Xpath {
335
                space0: Whitespace {
336
                    value: String::from(" "),
337
                    source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 7)),
338
                },
339
                expr: Template::new(
340
                    Some('"'),
341
                    vec![TemplateElement::String {
342
                        value: "normalize-space(//head/title)".to_string(),
343
                        source: "normalize-space(//head/title)".to_source(),
344
                    }],
345
                    SourceInfo::new(Pos::new(1, 7), Pos::new(1, 38))
346
                ),
347
            },
348
        );
349

            
350
        let mut reader = Reader::new("xpath \"normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])\"");
351
        assert_eq!(xpath_query(&mut reader).unwrap(), QueryValue::Xpath {
352
            space0: Whitespace { value: String::from(" "), source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 7)) },
353
            expr: Template::new(
354
                Some('"'),
355
                vec![
356
                    TemplateElement::String {
357
                        value: "normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])".to_string(),
358
                        source: "normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])".to_source(),
359
                    }
360
                ],
361
                SourceInfo::new(Pos::new(1, 7), Pos::new(1, 100))
362
            )
363
        });
364
    }
365

            
366
    #[test]
367
    fn test_jsonpath_query() {
368
        let mut reader = Reader::new("jsonpath \"$['statusCode']\"");
369
        assert_eq!(
370
            jsonpath_query(&mut reader).unwrap(),
371
            QueryValue::Jsonpath {
372
                space0: Whitespace {
373
                    value: String::from(" "),
374
                    source_info: SourceInfo::new(Pos::new(1, 9), Pos::new(1, 10)),
375
                },
376
                expr: Template::new(
377
                    Some('"'),
378
                    vec![TemplateElement::String {
379
                        value: "$['statusCode']".to_string(),
380
                        source: "$['statusCode']".to_source(),
381
                    }],
382
                    SourceInfo::new(Pos::new(1, 10), Pos::new(1, 27))
383
                )
384
            }
385
        );
386
        let mut reader = Reader::new("jsonpath \"$.success\"");
387
        assert_eq!(
388
            jsonpath_query(&mut reader).unwrap(),
389
            QueryValue::Jsonpath {
390
                space0: Whitespace {
391
                    value: String::from(" "),
392
                    source_info: SourceInfo::new(Pos::new(1, 9), Pos::new(1, 10)),
393
                },
394
                expr: Template::new(
395
                    Some('"'),
396
                    vec![TemplateElement::String {
397
                        value: "$.success".to_string(),
398
                        source: "$.success".to_source(),
399
                    }],
400
                    SourceInfo::new(Pos::new(1, 10), Pos::new(1, 21))
401
                )
402
            }
403
        );
404
    }
405

            
406
    #[test]
407
    fn test_query_with_filters() {
408
        let mut reader = Reader::new("body urlDecode ");
409
        assert_eq!(
410
            query(&mut reader).unwrap(),
411
            Query {
412
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 5)),
413
                value: QueryValue::Body,
414
            }
415
        );
416
        assert_eq!(
417
            filters(&mut reader).unwrap(),
418
            vec![(
419
                Whitespace {
420
                    value: " ".to_string(),
421
                    source_info: SourceInfo::new(Pos::new(1, 5), Pos::new(1, 6))
422
                },
423
                Filter {
424
                    source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 15)),
425
                    value: FilterValue::UrlDecode,
426
                }
427
            )]
428
        );
429
        assert_eq!(reader.cursor().index, CharPos(14));
430
    }
431
}