1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2026 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
33030
pub fn query(reader: &mut Reader) -> ParseResult<Query> {
27
33030
    let start = reader.cursor();
28
33030
    let value = query_value(reader)?;
29
25340
    let end = reader.cursor();
30
25340
    Ok(Query {
31
25340
        source_info: SourceInfo::new(start.pos, end.pos),
32
25340
        value,
33
25340
    })
34
}
35

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

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

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

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

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

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

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

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

            
99
715
    Ok(QueryValue::Cookie { space0, expr })
100
}
101

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

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

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

            
132
10130
    Ok(QueryValue::Jsonpath { space0, expr })
133
}
134

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

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

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

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

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

            
181
9820
fn rawbytes_query(reader: &mut Reader) -> ParseResult<QueryValue> {
182
9820
    try_literal("rawbytes", reader)?;
183
380
    Ok(QueryValue::RawBytes)
184
}
185

            
186
9440
fn sha256_query(reader: &mut Reader) -> ParseResult<QueryValue> {
187
9440
    try_literal("sha256", reader)?;
188
265
    Ok(QueryValue::Sha256)
189
}
190

            
191
9175
fn md5_query(reader: &mut Reader) -> ParseResult<QueryValue> {
192
9175
    try_literal("md5", reader)?;
193
235
    Ok(QueryValue::Md5)
194
}
195

            
196
8940
fn certificate_query(reader: &mut Reader) -> ParseResult<QueryValue> {
197
8940
    try_literal("certificate", reader)?;
198
360
    let space0 = one_or_more_spaces(reader)?;
199
360
    let field = certificate_field(reader)?;
200
360
    Ok(QueryValue::Certificate {
201
360
        space0,
202
360
        attribute_name: field,
203
360
    })
204
}
205

            
206
8580
fn ip_query(reader: &mut Reader) -> ParseResult<QueryValue> {
207
8580
    try_literal("ip", reader)?;
208
90
    Ok(QueryValue::Ip)
209
}
210

            
211
8490
fn redirects_query(reader: &mut Reader) -> ParseResult<QueryValue> {
212
8490
    try_literal("redirects", reader)?;
213
825
    Ok(QueryValue::Redirects)
214
}
215

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

            
239
#[cfg(test)]
240
mod tests {
241
    use super::*;
242
    use crate::ast::{
243
        CookieAttribute, CookieAttributeName, CookiePath, Filter, FilterValue, Template,
244
        TemplateElement, Whitespace,
245
    };
246
    use crate::parser::filter::filters;
247
    use crate::reader::{CharPos, Pos};
248
    use crate::types::ToSource;
249

            
250
    #[test]
251
    fn test_query() {
252
        let mut reader = Reader::new("status");
253
        assert_eq!(
254
            query(&mut reader).unwrap(),
255
            Query {
256
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
257
                value: QueryValue::Status,
258
            }
259
        );
260
    }
261

            
262
    #[test]
263
    fn test_status_query() {
264
        let mut reader = Reader::new("status");
265
        assert_eq!(
266
            query(&mut reader).unwrap(),
267
            Query {
268
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
269
                value: QueryValue::Status,
270
            }
271
        );
272
    }
273

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

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

            
331
        // todo test with escape sequence
332
        //let mut reader = Reader::init("cookie \"cookie\u{31}\"");
333
    }
334

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

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

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

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

            
438
    #[test]
439
    fn test_bytes_query() {
440
        let mut reader = Reader::new("bytes");
441
        assert_eq!(
442
            query(&mut reader).unwrap(),
443
            Query {
444
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 6)),
445
                value: QueryValue::Bytes,
446
            }
447
        );
448
    }
449

            
450
    #[test]
451
    fn test_rawbytes_query() {
452
        let mut reader = Reader::new("rawbytes");
453
        assert_eq!(
454
            query(&mut reader).unwrap(),
455
            Query {
456
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 9)),
457
                value: QueryValue::RawBytes,
458
            }
459
        );
460
    }
461
}