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

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

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

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

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

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

            
82
25910
fn cookie_query(reader: &mut Reader) -> ParseResult<QueryValue> {
83
25910
    try_literal("cookie", reader)?;
84
640
    let space0 = one_or_more_spaces(reader)?;
85

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

            
92
    // We will parse the cookiepath value without `"`.
93
635
    let pos = Pos::new(start.pos.line, start.pos.column + 1);
94
635
    let mut cookiepath_reader = Reader::with_pos(s.as_str(), pos);
95
635
    let expr = cookiepath(&mut cookiepath_reader)?;
96

            
97
630
    Ok(QueryValue::Cookie { space0, expr })
98
}
99

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

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

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

            
130
7365
    Ok(QueryValue::Jsonpath { space0, expr })
131
}
132

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

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

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

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

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

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

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

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

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

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

            
225
#[cfg(test)]
226
mod tests {
227
    use super::*;
228
    use crate::ast::{
229
        CookieAttribute, CookieAttributeName, CookiePath, Filter, FilterValue, Template,
230
        TemplateElement, Whitespace,
231
    };
232
    use crate::parser::filter::filters;
233
    use crate::reader::Pos;
234
    use crate::typing::ToSource;
235

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

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

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

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

            
317
        // todo test with escape sequence
318
        //let mut reader = Reader::init("cookie \"cookie\u{31}\"");
319
    }
320

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

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

            
358
    #[test]
359
    fn test_jsonpath_query() {
360
        let mut reader = Reader::new("jsonpath \"$['statusCode']\"");
361
        assert_eq!(
362
            jsonpath_query(&mut reader).unwrap(),
363
            QueryValue::Jsonpath {
364
                space0: Whitespace {
365
                    value: String::from(" "),
366
                    source_info: SourceInfo::new(Pos::new(1, 9), Pos::new(1, 10)),
367
                },
368
                expr: Template::new(
369
                    Some('"'),
370
                    vec![TemplateElement::String {
371
                        value: "$['statusCode']".to_string(),
372
                        source: "$['statusCode']".to_source(),
373
                    }],
374
                    SourceInfo::new(Pos::new(1, 10), Pos::new(1, 27))
375
                )
376
            }
377
        );
378
        let mut reader = Reader::new("jsonpath \"$.success\"");
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: "$.success".to_string(),
390
                        source: "$.success".to_source(),
391
                    }],
392
                    SourceInfo::new(Pos::new(1, 10), Pos::new(1, 21))
393
                )
394
            }
395
        );
396
    }
397

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