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

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

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

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

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

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

            
84
31700
fn cookie_query(reader: &mut Reader) -> ParseResult<QueryValue> {
85
31700
    try_literal("cookie", reader)?;
86
740
    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
740
    let start = reader.cursor();
90
740
    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
735
    let pos = Pos::new(start.pos.line, start.pos.column + 1);
96
735
    let mut cookiepath_reader = Reader::with_pos(s.as_str(), pos);
97
735
    let expr = cookiepath(&mut cookiepath_reader)?;
98

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

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

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

            
114
23270
fn jsonpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
115
23270
    try_literal("jsonpath", reader)?;
116
10505
    let space0 = one_or_more_spaces(reader)?;
117
    //let expr = jsonpath_expr(reader)?;
118
    //  let start = reader.state.pos.clone();
119
10506
    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
10500
    Ok(QueryValue::Jsonpath { space0, expr })
133
}
134

            
135
12765
fn regex_query(reader: &mut Reader) -> ParseResult<QueryValue> {
136
12765
    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
12600
fn variable_query(reader: &mut Reader) -> ParseResult<QueryValue> {
165
12600
    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
11430
fn duration_query(reader: &mut Reader) -> ParseResult<QueryValue> {
172
11430
    try_literal("duration", reader)?;
173
95
    Ok(QueryValue::Duration)
174
}
175

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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