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

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

            
58
27515
fn status_query(reader: &mut Reader) -> ParseResult<QueryValue> {
59
27515
    try_literal("status", reader)?;
60
100
    Ok(QueryValue::Status)
61
}
62

            
63
27415
fn url_query(reader: &mut Reader) -> ParseResult<QueryValue> {
64
27415
    try_literal("url", reader)?;
65
215
    Ok(QueryValue::Url)
66
}
67

            
68
27200
fn header_query(reader: &mut Reader) -> ParseResult<QueryValue> {
69
27200
    try_literal("header", reader)?;
70
1400
    let space0 = one_or_more_spaces(reader)?;
71
1400
    let name = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
72
1400
    Ok(QueryValue::Header { space0, name })
73
}
74

            
75
25800
fn cookie_query(reader: &mut Reader) -> ParseResult<QueryValue> {
76
25800
    try_literal("cookie", reader)?;
77
640
    let space0 = one_or_more_spaces(reader)?;
78

            
79
    // Read the whole value of the coookie path and parse it with a specialized reader.
80
640
    let start = reader.cursor();
81
640
    let s = quoted_oneline_string(reader)?;
82
    // todo should work with an encodedString in order to support escape sequence
83
    // or decode escape sequence with the cookiepath parser
84

            
85
    // We will parse the cookiepath value without `"`.
86
635
    let pos = Pos::new(start.pos.line, start.pos.column + 1);
87
635
    let mut cookiepath_reader = Reader::with_pos(s.as_str(), pos);
88
635
    let expr = cookiepath(&mut cookiepath_reader)?;
89

            
90
630
    Ok(QueryValue::Cookie { space0, expr })
91
}
92

            
93
25160
fn body_query(reader: &mut Reader) -> ParseResult<QueryValue> {
94
25160
    try_literal("body", reader)?;
95
6125
    Ok(QueryValue::Body)
96
}
97

            
98
19035
fn xpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
99
19035
    try_literal("xpath", reader)?;
100
1420
    let space0 = one_or_more_spaces(reader)?;
101
1421
    let expr = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
102
1415
    Ok(QueryValue::Xpath { space0, expr })
103
}
104

            
105
17615
fn jsonpath_query(reader: &mut Reader) -> ParseResult<QueryValue> {
106
17615
    try_literal("jsonpath", reader)?;
107
7360
    let space0 = one_or_more_spaces(reader)?;
108
    //let expr = jsonpath_expr(reader)?;
109
    //  let start = reader.state.pos.clone();
110
7361
    let expr = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
111
    //    let end = reader.state.pos.clone();
112
    //    let expr = Template {
113
    //        elements: template.elements.iter().map(|e| match e {
114
    //            TemplateElement::String { value, encoded } => HurlTemplateElement::Literal {
115
    //                value: HurlString2 { value: value.clone(), encoded: Some(encoded.clone()) }
116
    //            },
117
    //            TemplateElement::Expression(value) => HurlTemplateElement::Expression { value: value.clone() }
118
    //        }).collect(),
119
    //        quotes: true,
120
    //        source_info: SourceInfo { start, end },
121
    //    };
122

            
123
7355
    Ok(QueryValue::Jsonpath { space0, expr })
124
}
125

            
126
10255
fn regex_query(reader: &mut Reader) -> ParseResult<QueryValue> {
127
10255
    try_literal("regex", reader)?;
128
120
    let space0 = one_or_more_spaces(reader)?;
129
120
    let value = regex_value(reader)?;
130
115
    Ok(QueryValue::Regex { space0, value })
131
}
132

            
133
490
pub fn regex_value(reader: &mut Reader) -> ParseResult<RegexValue> {
134
490
    choice(
135
490
        &[
136
588
            |p1| match quoted_template(p1) {
137
325
                Ok(value) => Ok(RegexValue::Template(value)),
138
165
                Err(e) => Err(e),
139
588
            },
140
523
            |p1| match regex(p1) {
141
160
                Ok(value) => Ok(RegexValue::Regex(value)),
142
5
                Err(e) => Err(e),
143
523
            },
144
490
        ],
145
490
        reader,
146
490
    )
147
491
    .map_err(|e| {
148
5
        let kind = ParseErrorKind::Expecting {
149
5
            value: "\" or /".to_string(),
150
5
        };
151
5
        ParseError::new(e.pos, false, kind)
152
491
    })
153
}
154

            
155
10135
fn variable_query(reader: &mut Reader) -> ParseResult<QueryValue> {
156
10135
    try_literal("variable", reader)?;
157
1080
    let space0 = one_or_more_spaces(reader)?;
158
1080
    let name = quoted_template(reader).map_err(|e| e.to_non_recoverable())?;
159
1080
    Ok(QueryValue::Variable { space0, name })
160
}
161

            
162
9055
fn duration_query(reader: &mut Reader) -> ParseResult<QueryValue> {
163
9055
    try_literal("duration", reader)?;
164
80
    Ok(QueryValue::Duration)
165
}
166

            
167
8975
fn bytes_query(reader: &mut Reader) -> ParseResult<QueryValue> {
168
8975
    try_literal("bytes", reader)?;
169
920
    Ok(QueryValue::Bytes)
170
}
171

            
172
8055
fn sha256_query(reader: &mut Reader) -> ParseResult<QueryValue> {
173
8055
    try_literal("sha256", reader)?;
174
220
    Ok(QueryValue::Sha256)
175
}
176

            
177
7835
fn md5_query(reader: &mut Reader) -> ParseResult<QueryValue> {
178
7835
    try_literal("md5", reader)?;
179
210
    Ok(QueryValue::Md5)
180
}
181

            
182
7625
fn certificate_query(reader: &mut Reader) -> ParseResult<QueryValue> {
183
7625
    try_literal("certificate", reader)?;
184
290
    let space0 = one_or_more_spaces(reader)?;
185
290
    let field = certificate_field(reader)?;
186
290
    Ok(QueryValue::Certificate {
187
290
        space0,
188
290
        attribute_name: field,
189
290
    })
190
}
191

            
192
290
fn certificate_field(reader: &mut Reader) -> ParseResult<CertificateAttributeName> {
193
290
    literal("\"", reader)?;
194
290
    if try_literal(r#"Subject""#, reader).is_ok() {
195
55
        Ok(CertificateAttributeName::Subject)
196
235
    } else if try_literal(r#"Issuer""#, reader).is_ok() {
197
35
        Ok(CertificateAttributeName::Issuer)
198
200
    } else if try_literal(r#"Start-Date""#, reader).is_ok() {
199
55
        Ok(CertificateAttributeName::StartDate)
200
145
    } else if try_literal(r#"Expire-Date""#, reader).is_ok() {
201
110
        Ok(CertificateAttributeName::ExpireDate)
202
35
    } else if try_literal(r#"Serial-Number""#, reader).is_ok() {
203
35
        Ok(CertificateAttributeName::SerialNumber)
204
    } else {
205
        let value =
206
            "Field <Subject>, <Issuer>, <Start-Date>, <Expire-Date> or <Serial-Number>".to_string();
207
        let kind = ParseErrorKind::Expecting { value };
208
        let cur = reader.cursor();
209
        Err(ParseError::new(cur.pos, false, kind))
210
    }
211
}
212

            
213
#[cfg(test)]
214
mod tests {
215
    use super::*;
216
    use crate::ast::{
217
        CookieAttribute, CookieAttributeName, CookiePath, Filter, FilterValue, Template,
218
        TemplateElement, Whitespace,
219
    };
220
    use crate::parser::filter::filters;
221
    use crate::reader::Pos;
222

            
223
    #[test]
224
    fn test_query() {
225
        let mut reader = Reader::new("status");
226
        assert_eq!(
227
            query(&mut reader).unwrap(),
228
            Query {
229
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
230
                value: QueryValue::Status,
231
            }
232
        );
233
    }
234

            
235
    #[test]
236
    fn test_status_query() {
237
        let mut reader = Reader::new("status");
238
        assert_eq!(
239
            query(&mut reader).unwrap(),
240
            Query {
241
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
242
                value: QueryValue::Status,
243
            }
244
        );
245
    }
246

            
247
    #[test]
248
    fn test_header_query() {
249
        let mut reader = Reader::new("header \"Foo\"");
250
        assert_eq!(
251
            header_query(&mut reader).unwrap(),
252
            QueryValue::Header {
253
                space0: Whitespace {
254
                    value: String::from(" "),
255
                    source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 8)),
256
                },
257
                name: Template {
258
                    delimiter: Some('"'),
259
                    elements: vec![TemplateElement::String {
260
                        value: "Foo".to_string(),
261
                        encoded: "Foo".to_string(),
262
                    }],
263
                    source_info: SourceInfo::new(Pos::new(1, 8), Pos::new(1, 13)),
264
                },
265
            }
266
        );
267
    }
268

            
269
    #[test]
270
    fn test_cookie_query() {
271
        let mut reader = Reader::new("cookie \"Foo[Domain]\"");
272
        assert_eq!(
273
            cookie_query(&mut reader).unwrap(),
274
            QueryValue::Cookie {
275
                space0: Whitespace {
276
                    value: String::from(" "),
277
                    source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 8)),
278
                },
279
                expr: CookiePath {
280
                    name: Template {
281
                        delimiter: None,
282
                        elements: vec![TemplateElement::String {
283
                            value: "Foo".to_string(),
284
                            encoded: "Foo".to_string(),
285
                        }],
286
                        source_info: SourceInfo::new(Pos::new(1, 9), Pos::new(1, 12)),
287
                    },
288
                    attribute: Some(CookieAttribute {
289
                        space0: Whitespace {
290
                            value: String::new(),
291
                            source_info: SourceInfo::new(Pos::new(1, 13), Pos::new(1, 13)),
292
                        },
293
                        name: CookieAttributeName::Domain("Domain".to_string()),
294
                        space1: Whitespace {
295
                            value: String::new(),
296
                            source_info: SourceInfo::new(Pos::new(1, 19), Pos::new(1, 19)),
297
                        },
298
                    }),
299
                },
300
            }
301
        );
302
        assert_eq!(reader.cursor().index, 20);
303

            
304
        // todo test with escape sequence
305
        //let mut reader = Reader::init("cookie \"cookie\u{31}\"");
306
    }
307

            
308
    #[test]
309
    fn test_xpath_query() {
310
        let mut reader = Reader::new("xpath \"normalize-space(//head/title)\"");
311
        assert_eq!(
312
            xpath_query(&mut reader).unwrap(),
313
            QueryValue::Xpath {
314
                space0: Whitespace {
315
                    value: String::from(" "),
316
                    source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 7)),
317
                },
318
                expr: Template {
319
                    delimiter: Some('"'),
320
                    elements: vec![TemplateElement::String {
321
                        value: String::from("normalize-space(//head/title)"),
322
                        encoded: String::from("normalize-space(//head/title)"),
323
                    }],
324
                    source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 38)),
325
                },
326
            },
327
        );
328

            
329
        let mut reader = Reader::new("xpath \"normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])\"");
330
        assert_eq!(xpath_query(&mut reader).unwrap(), QueryValue::Xpath {
331
            space0: Whitespace { value: String::from(" "), source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 7)) },
332
            expr: Template {
333
                delimiter: Some('"'),
334
                elements: vec![
335
                    TemplateElement::String {
336
                        value: String::from("normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])"),
337
                        encoded: String::from("normalize-space(//div[contains(concat(' ',normalize-space(@class),' '),' monthly-price ')])"),
338
                    }
339
                ],
340
                source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 100)),
341
            },
342

            
343
        });
344
    }
345

            
346
    #[test]
347
    fn test_jsonpath_query() {
348
        let mut reader = Reader::new("jsonpath \"$['statusCode']\"");
349
        assert_eq!(
350
            jsonpath_query(&mut reader).unwrap(),
351
            QueryValue::Jsonpath {
352
                space0: Whitespace {
353
                    value: String::from(" "),
354
                    source_info: SourceInfo::new(Pos::new(1, 9), Pos::new(1, 10)),
355
                },
356
                expr: Template {
357
                    elements: vec![TemplateElement::String {
358
                        value: "$['statusCode']".to_string(),
359
                        encoded: "$['statusCode']".to_string(),
360
                    }],
361
                    delimiter: Some('"'),
362
                    source_info: SourceInfo::new(Pos::new(1, 10), Pos::new(1, 27)),
363
                },
364
            },
365
        );
366
        let mut reader = Reader::new("jsonpath \"$.success\"");
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 {
375
                    elements: vec![TemplateElement::String {
376
                        value: "$.success".to_string(),
377
                        encoded: "$.success".to_string(),
378
                    }],
379
                    delimiter: Some('"'),
380
                    source_info: SourceInfo::new(Pos::new(1, 10), Pos::new(1, 21)),
381
                },
382
            },
383
        );
384
    }
385

            
386
    #[test]
387
    fn test_query_with_filters() {
388
        let mut reader = Reader::new("body urlDecode ");
389
        assert_eq!(
390
            query(&mut reader).unwrap(),
391
            Query {
392
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 5)),
393
                value: QueryValue::Body,
394
            }
395
        );
396
        assert_eq!(
397
            filters(&mut reader).unwrap(),
398
            vec![(
399
                Whitespace {
400
                    value: " ".to_string(),
401
                    source_info: SourceInfo::new(Pos::new(1, 5), Pos::new(1, 6))
402
                },
403
                Filter {
404
                    source_info: SourceInfo::new(Pos::new(1, 6), Pos::new(1, 15)),
405
                    value: FilterValue::UrlDecode,
406
                }
407
            )]
408
        );
409
        assert_eq!(reader.cursor().index, 14);
410
    }
411
}