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::{ParseError as ParseErrorTrait, choice};
20
use crate::reader::{Pos, Reader};
21

            
22
use super::ParseResult;
23
use super::cookiepath::cookiepath;
24
use super::error::{ParseError, ParseErrorKind};
25
use super::primitives::{literal, one_or_more_spaces, regex, try_literal};
26
use super::string::{quoted_oneline_string, quoted_template};
27

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

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

            
64
29404
fn status_query(reader: &mut Reader) -> ParseResult<QueryValue> {
65
29404
    try_literal("status", reader)?;
66
96
    Ok(QueryValue::Status)
67
}
68

            
69
29308
fn version_query(reader: &mut Reader) -> ParseResult<QueryValue> {
70
29308
    try_literal("version", reader)?;
71
104
    Ok(QueryValue::Version)
72
}
73

            
74
29204
fn url_query(reader: &mut Reader) -> ParseResult<QueryValue> {
75
29204
    try_literal("url", reader)?;
76
552
    Ok(QueryValue::Url)
77
}
78

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

            
86
27216
fn cookie_query(reader: &mut Reader) -> ParseResult<QueryValue> {
87
27216
    try_literal("cookie", reader)?;
88
604
    let space0 = one_or_more_spaces(reader)?;
89

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

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

            
101
596
    Ok(QueryValue::Cookie { space0, expr })
102
}
103

            
104
26612
fn body_query(reader: &mut Reader) -> ParseResult<QueryValue> {
105
26612
    try_literal("body", reader)?;
106
5040
    Ok(QueryValue::Body)
107
}
108

            
109
21572
fn xpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
110
21572
    try_literal("xpath", reader)?;
111
1168
    let space0 = one_or_more_spaces(reader)?;
112
1168
    let expr = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
113
1164
    Ok(QueryValue::Xpath { space0, expr })
114
}
115

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

            
134
9588
    Ok(QueryValue::Jsonpath { space0, expr })
135
}
136

            
137
10812
fn regex_query(reader: &mut Reader) -> ParseResult<QueryValue> {
138
10812
    try_literal("regex", reader)?;
139
132
    let space0 = one_or_more_spaces(reader)?;
140
132
    let value = regex_value(reader)?;
141
128
    Ok(QueryValue::Regex { space0, value })
142
}
143

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

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

            
173
9740
fn duration_query(reader: &mut Reader) -> ParseResult<QueryValue> {
174
9740
    try_literal("duration", reader)?;
175
84
    Ok(QueryValue::Duration)
176
}
177

            
178
9656
fn bytes_query(reader: &mut Reader) -> ParseResult<QueryValue> {
179
9656
    try_literal("bytes", reader)?;
180
888
    Ok(QueryValue::Bytes)
181
}
182

            
183
8768
fn rawbytes_query(reader: &mut Reader) -> ParseResult<QueryValue> {
184
8768
    try_literal("rawbytes", reader)?;
185
384
    Ok(QueryValue::RawBytes)
186
}
187

            
188
8384
fn sha256_query(reader: &mut Reader) -> ParseResult<QueryValue> {
189
8384
    try_literal("sha256", reader)?;
190
252
    Ok(QueryValue::Sha256)
191
}
192

            
193
8132
fn md5_query(reader: &mut Reader) -> ParseResult<QueryValue> {
194
8132
    try_literal("md5", reader)?;
195
228
    Ok(QueryValue::Md5)
196
}
197

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

            
208
7576
fn ip_query(reader: &mut Reader) -> ParseResult<QueryValue> {
209
7576
    try_literal("ip", reader)?;
210
104
    Ok(QueryValue::Ip)
211
}
212

            
213
7472
fn redirects_query(reader: &mut Reader) -> ParseResult<QueryValue> {
214
7472
    try_literal("redirects", reader)?;
215
984
    Ok(QueryValue::Redirects)
216
}
217

            
218
328
fn certificate_field(reader: &mut Reader) -> ParseResult<CertificateAttributeName> {
219
328
    literal("\"", reader)?;
220
328
    if try_literal(r#"Subject""#, reader).is_ok() {
221
64
        Ok(CertificateAttributeName::Subject)
222
264
    } else if try_literal(r#"Issuer""#, reader).is_ok() {
223
36
        Ok(CertificateAttributeName::Issuer)
224
228
    } else if try_literal(r#"Start-Date""#, reader).is_ok() {
225
80
        Ok(CertificateAttributeName::StartDate)
226
148
    } else if try_literal(r#"Expire-Date""#, reader).is_ok() {
227
112
        Ok(CertificateAttributeName::ExpireDate)
228
36
    } else if try_literal(r#"Serial-Number""#, reader).is_ok() {
229
36
        Ok(CertificateAttributeName::SerialNumber)
230
    } else if try_literal(r#"Subject-Alt-Name""#, reader).is_ok() {
231
        Ok(CertificateAttributeName::SubjectAltName)
232
    } else if try_literal(r#"Value""#, reader).is_ok() {
233
        Ok(CertificateAttributeName::Value)
234
    } else {
235
        let value =
236
            "Field <Subject>, <Issuer>, <Start-Date>, <Expire-Date>, <Serial-Number>, <Subject-Alt-Name> or <Value>".to_string();
237
        let kind = ParseErrorKind::Expecting { value };
238
        let cur = reader.cursor();
239
        Err(ParseError::new(cur.pos, false, kind))
240
    }
241
}
242

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

            
253
    use super::*;
254

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

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

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

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

            
336
        // todo test with escape sequence
337
        //let mut reader = Reader::init("cookie \"cookie\u{31}\"");
338
    }
339

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

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

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

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

            
445
    #[test]
446
    fn test_bytes_query() {
447
        let mut reader = Reader::new("bytes");
448
        assert_eq!(
449
            query(&mut reader).unwrap(),
450
            Query {
451
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 6)),
452
                value: QueryValue::Bytes,
453
            }
454
        );
455
    }
456

            
457
    #[test]
458
    fn test_rawbytes_query() {
459
        let mut reader = Reader::new("rawbytes");
460
        assert_eq!(
461
            query(&mut reader).unwrap(),
462
            Query {
463
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 9)),
464
                value: QueryValue::RawBytes,
465
            }
466
        );
467
    }
468
}