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

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

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

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

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

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

            
84
30805
fn cookie_query(reader: &mut Reader) -> ParseResult<QueryValue> {
85
30805
    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
30080
fn body_query(reader: &mut Reader) -> ParseResult<QueryValue> {
103
30080
    try_literal("body", reader)?;
104
6200
    Ok(QueryValue::Body)
105
}
106

            
107
23880
fn xpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
108
23880
    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
22450
fn jsonpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
115
22450
    try_literal("jsonpath", reader)?;
116
10140
    let space0 = one_or_more_spaces(reader)?;
117
    //let expr = jsonpath_expr(reader)?;
118
    //  let start = reader.state.pos.clone();
119
10141
    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
10135
    Ok(QueryValue::Jsonpath { space0, expr })
133
}
134

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

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

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

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

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

            
196
8970
fn certificate_query(reader: &mut Reader) -> ParseResult<QueryValue> {
197
8970
    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
8610
fn ip_query(reader: &mut Reader) -> ParseResult<QueryValue> {
207
8610
    try_literal("ip", reader)?;
208
120
    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 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("xpath \"normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])\"");
359
        assert_eq!(xpath_query(&mut reader).unwrap(), QueryValue::Xpath {
360
            space0: Whitespace { value: String::from(" "), source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 7)) },
361
            expr: Template::new(
362
                Some('"'),
363
                vec![
364
                    TemplateElement::String {
365
                        value: "normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])".to_string(),
366
                        source: "normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])".to_source(),
367
                    }
368
                ],
369
                SourceInfo::new(Pos::new(1, 7), Pos::new(1, 100))
370
            )
371
        });
372
    }
373

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

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

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

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