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

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

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

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

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

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

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

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

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

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

            
113
18610
fn jsonpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
114
18610
    try_literal("jsonpath", reader)?;
115
8130
    let space0 = one_or_more_spaces(reader)?;
116
    //let expr = jsonpath_expr(reader)?;
117
    //  let start = reader.state.pos.clone();
118
8131
    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
8125
    Ok(QueryValue::Jsonpath { space0, expr })
132
}
133

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

            
141
490
pub fn regex_value(reader: &mut Reader) -> ParseResult<RegexValue> {
142
490
    choice(
143
490
        &[
144
588
            |p1| match quoted_template(p1) {
145
325
                Ok(value) => Ok(RegexValue::Template(value)),
146
165
                Err(e) => Err(e),
147
588
            },
148
523
            |p1| match regex(p1) {
149
160
                Ok(value) => Ok(RegexValue::Regex(value)),
150
5
                Err(e) => Err(e),
151
523
            },
152
490
        ],
153
490
        reader,
154
490
    )
155
491
    .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
491
    })
161
}
162

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

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

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

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

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

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

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

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

            
210
290
fn certificate_field(reader: &mut Reader) -> ParseResult<CertificateAttributeName> {
211
290
    literal("\"", reader)?;
212
290
    if try_literal(r#"Subject""#, reader).is_ok() {
213
55
        Ok(CertificateAttributeName::Subject)
214
235
    } else if try_literal(r#"Issuer""#, reader).is_ok() {
215
35
        Ok(CertificateAttributeName::Issuer)
216
200
    } else if try_literal(r#"Start-Date""#, reader).is_ok() {
217
55
        Ok(CertificateAttributeName::StartDate)
218
145
    } else if try_literal(r#"Expire-Date""#, reader).is_ok() {
219
110
        Ok(CertificateAttributeName::ExpireDate)
220
35
    } else if try_literal(r#"Serial-Number""#, reader).is_ok() {
221
35
        Ok(CertificateAttributeName::SerialNumber)
222
    } else {
223
        let value =
224
            "Field <Subject>, <Issuer>, <Start-Date>, <Expire-Date> or <Serial-Number>".to_string();
225
        let kind = ParseErrorKind::Expecting { value };
226
        let cur = reader.cursor();
227
        Err(ParseError::new(cur.pos, false, kind))
228
    }
229
}
230

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

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

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

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

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

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

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

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

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

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