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

            
19
use std::fmt;
20

            
21
mod args;
22
mod commands;
23
mod matches;
24

            
25
#[derive(Clone, Debug, PartialEq, Eq)]
26
pub struct HurlOption {
27
    name: String,
28
    value: String,
29
}
30

            
31
impl HurlOption {
32
9
    pub fn new(name: &str, value: &str) -> HurlOption {
33
9
        HurlOption {
34
9
            name: name.to_string(),
35
9
            value: value.to_string(),
36
        }
37
    }
38
}
39

            
40
impl fmt::Display for HurlOption {
41
9
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42
9
        write!(f, "{}: {}", self.name, self.value)
43
    }
44
}
45

            
46
3
pub fn parse(s: &str) -> Result<String, String> {
47
3
    let cleaned_s = s.replace("\\\n", "").replace("\\\r\n", "");
48
3

            
49
3
    let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
50
3
        .unwrap()
51
3
        .split(&cleaned_s)
52
34
        .filter(|s| !s.is_empty())
53
3
        .collect();
54
3
    let mut s = String::new();
55
27
    for (i, line) in lines.iter().enumerate() {
56
27
        let hurl_str = parse_line(line).map_err(|message| {
57
            format!("Can not parse curl command at line {}: {message}", i + 1)
58
27
        })?;
59
27
        s.push_str(format!("{hurl_str}\n").as_str());
60
    }
61
3
    Ok(s)
62
}
63

            
64
27
fn parse_line(s: &str) -> Result<String, String> {
65
27
    let mut command = clap::Command::new("curl")
66
27
        .arg(commands::compressed())
67
27
        .arg(commands::data())
68
27
        .arg(commands::headers())
69
27
        .arg(commands::insecure())
70
27
        .arg(commands::location())
71
27
        .arg(commands::max_redirects())
72
27
        .arg(commands::method())
73
27
        .arg(commands::retry())
74
27
        .arg(commands::url())
75
27
        .arg(commands::url_param());
76

            
77
27
    let params = args::split(s)?;
78
27
    let arg_matches = match command.try_get_matches_from_mut(params) {
79
27
        Ok(r) => r,
80
        Err(e) => return Err(e.to_string()),
81
    };
82

            
83
27
    let method = matches::method(&arg_matches);
84
27
    let url = matches::url(&arg_matches);
85
27
    let headers = matches::headers(&arg_matches);
86
27
    let options = matches::options(&arg_matches);
87
27
    let body = matches::body(&arg_matches);
88
27
    let s = format(&method, &url, headers, &options, body);
89
27
    Ok(s)
90
}
91

            
92
27
fn format(
93
27
    method: &str,
94
27
    url: &str,
95
27
    headers: Vec<String>,
96
27
    options: &[HurlOption],
97
27
    body: Option<String>,
98
27
) -> String {
99
27
    let mut s = format!("{method} {url}");
100
51
    for header in headers {
101
24
        if let Some(stripped) = header.strip_suffix(";") {
102
3
            s.push_str(format!("\n{}:", stripped).as_str());
103
21
        } else {
104
21
            s.push_str(format!("\n{header}").as_str());
105
        }
106
    }
107
27
    if !options.is_empty() {
108
9
        s.push_str("\n[Options]");
109
18
        for option in options {
110
9
            s.push_str(format!("\n{option}").as_str());
111
        }
112
    }
113
27
    if let Some(body) = body {
114
6
        s.push('\n');
115
6
        s.push_str(body.as_str());
116
    }
117
27
    let asserts = additional_asserts(options);
118
27
    if !asserts.is_empty() {
119
3
        s.push_str("\nHTTP *");
120
3
        s.push_str("\n[Asserts]");
121
6
        for assert in asserts {
122
3
            s.push_str(format!("\n{assert}").as_str());
123
        }
124
    }
125
27
    s.push('\n');
126
27
    s
127
}
128

            
129
27
fn has_option(options: &[HurlOption], name: &str) -> bool {
130
33
    for option in options {
131
9
        if option.name == name {
132
3
            return true;
133
        }
134
    }
135
24
    false
136
}
137

            
138
27
fn additional_asserts(options: &[HurlOption]) -> Vec<String> {
139
27
    let mut asserts = vec![];
140
27
    if has_option(options, "retry") {
141
3
        asserts.push("status < 500".to_string());
142
    }
143
27
    asserts
144
}
145

            
146
#[cfg(test)]
147
mod test {
148
    use crate::curl::*;
149

            
150
    #[test]
151
    fn test_parse() {
152
        let hurl_str = r#"GET http://localhost:8000/hello
153

            
154
GET http://localhost:8000/custom-headers
155
Fruit:Raspberry
156

            
157
"#;
158
        assert_eq!(
159
            parse(
160
                r#"curl http://localhost:8000/hello
161
curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry'
162
"#
163
            )
164
            .unwrap(),
165
            hurl_str
166
        );
167
    }
168

            
169
    #[test]
170
    fn test_parse_with_escape() {
171
        let hurl_str = r#"GET http://localhost:8000/custom_headers
172
Fruit:Raspberry
173
Fruit:Banana
174

            
175
"#;
176

            
177
        assert_eq!(
178
            parse(
179
                r#"curl http://localhost:8000/custom_headers \
180
                -H 'Fruit:Raspberry' \
181
                -H 'Fruit:Banana'
182
"#,
183
            )
184
            .unwrap(),
185
            hurl_str
186
        );
187
    }
188

            
189
    #[test]
190
    fn test_hello() {
191
        let hurl_str = r#"GET http://localhost:8000/hello
192
"#;
193
        assert_eq!(
194
            parse_line("curl http://localhost:8000/hello").unwrap(),
195
            hurl_str
196
        );
197
    }
198

            
199
    #[test]
200
    fn test_headers() {
201
        let hurl_str = r#"GET http://localhost:8000/custom-headers
202
Fruit:Raspberry
203
Fruit: Banana
204
Test: '
205
"#;
206
        assert_eq!(
207
            parse_line("curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' -H 'Fruit: Banana' -H $'Test: \\''").unwrap(),
208
            hurl_str
209
        );
210
        assert_eq!(
211
            parse_line("curl http://localhost:8000/custom-headers   --header Fruit:Raspberry -H 'Fruit: Banana' -H $'Test: \\''  ").unwrap(),
212
            hurl_str
213
        );
214
    }
215

            
216
    #[test]
217
    fn test_empty_headers() {
218
        let hurl_str = r#"GET http://localhost:8000/empty-headers
219
Empty-Header:
220
"#;
221
        assert_eq!(
222
            parse_line("curl http://localhost:8000/empty-headers -H 'Empty-Header;'").unwrap(),
223
            hurl_str
224
        );
225
    }
226

            
227
    #[test]
228
    fn test_post_hello() {
229
        let hurl_str = r#"POST http://localhost:8000/hello
230
Content-Type: text/plain
231
```
232
hello
233
```
234
"#;
235
        assert_eq!(
236
            parse_line(r#"curl -d $'hello'  -H 'Content-Type: text/plain' -X POST http://localhost:8000/hello"#).unwrap(),
237
            hurl_str
238
        );
239
    }
240

            
241
    #[test]
242
    fn test_post_format_params() {
243
        let hurl_str = r#"POST http://localhost:3000/data
244
Content-Type: application/x-www-form-urlencoded
245
```
246
param1=value1&param2=value2
247
```
248
"#;
249
        assert_eq!(
250
            parse_line("curl http://localhost:3000/data -d 'param1=value1&param2=value2'").unwrap(),
251
            hurl_str
252
        );
253
        assert_eq!(
254
            parse_line("curl -X POST http://localhost:3000/data -H 'Content-Type: application/x-www-form-urlencoded' --data 'param1=value1&param2=value2'").unwrap(),
255
            hurl_str
256
        );
257
    }
258

            
259
    #[test]
260
    fn test_post_json() {
261
        let hurl_str = r#"POST http://localhost:3000/data
262
Content-Type: application/json
263
```
264
{"key1":"value1", "key2":"value2"}
265
```
266
"#;
267
        assert_eq!(
268
                hurl_str,
269
                parse_line(r#"curl -d '{"key1":"value1", "key2":"value2"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap()
270
            );
271

            
272
        let hurl_str = r#"POST http://localhost:3000/data
273
Content-Type: application/json
274
```
275
{
276
  "key1": "value1",
277
  "key2": "value2"
278
}
279
```
280
"#;
281
        assert_eq!(
282
            parse_line(r#"curl -d $'{\n  "key1": "value1",\n  "key2": "value2"\n}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap(),
283
            hurl_str
284
        );
285
    }
286

            
287
    #[test]
288
    fn test_post_file() {
289
        let hurl_str = r#"POST http://example.com/
290
file, filename;
291
"#;
292
        assert_eq!(
293
            parse_line(r#"curl --data @filename http://example.com/"#).unwrap(),
294
            hurl_str
295
        );
296
    }
297

            
298
    #[test]
299
    fn test_redirect() {
300
        let hurl_str = r#"GET http://localhost:8000/redirect-absolute
301
[Options]
302
location: true
303
"#;
304
        assert_eq!(
305
            parse_line(r#"curl -L http://localhost:8000/redirect-absolute"#).unwrap(),
306
            hurl_str
307
        );
308
    }
309

            
310
    #[test]
311
    fn test_insecure() {
312
        let hurl_str = r#"GET https://localhost:8001/hello
313
[Options]
314
insecure: true
315
"#;
316
        assert_eq!(
317
            parse_line(r#"curl -k https://localhost:8001/hello"#).unwrap(),
318
            hurl_str
319
        );
320
    }
321

            
322
    #[test]
323
    fn test_max_redirects() {
324
        let hurl_str = r#"GET https://localhost:8001/hello
325
[Options]
326
max-redirs: 10
327
"#;
328
        assert_eq!(
329
            parse_line(r#"curl https://localhost:8001/hello --max-redirs 10"#).unwrap(),
330
            hurl_str
331
        );
332
    }
333
}