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::{SourceInfo, Template, TemplateElement};
19
use crate::reader::Reader;
20
use crate::types::ToSource;
21

            
22
use super::error::{ParseError, ParseErrorKind};
23
use super::primitives::try_literal;
24
use super::{ParseResult, placeholder, string};
25

            
26
/// Parse a filename.
27
///
28
/// A few characters need to be escaped such as space
29
/// for example: file\ with\ space.txt
30
/// This is very similar to the behaviour in a standard shell.
31
///
32
880
pub fn parse(reader: &mut Reader) -> ParseResult<Template> {
33
880
    let start = reader.cursor();
34

            
35
880
    let mut elements = vec![];
36
    loop {
37
1760
        let start = reader.cursor();
38
1760
        match placeholder::parse(reader) {
39
68
            Ok(placeholder) => {
40
68
                let element = TemplateElement::Placeholder(placeholder);
41
68
                elements.push(element);
42
            }
43
1692
            Err(e) => {
44
1692
                if e.recoverable {
45
1692
                    let value = filename_content(reader)?;
46
1692
                    if value.is_empty() {
47
880
                        break;
48
                    }
49
812
                    let source = reader.read_from(start.index).to_source();
50
812
                    let element = TemplateElement::String { value, source };
51
812
                    elements.push(element);
52
                } else {
53
                    return Err(e);
54
                }
55
            }
56
        }
57
    }
58
880
    if elements.is_empty() {
59
8
        let kind = ParseErrorKind::Filename;
60
8
        return Err(ParseError::new(start.pos, false, kind));
61
    }
62
872
    if let Some(TemplateElement::String { source, .. }) = elements.first()
63
808
        && source.starts_with('[')
64
    {
65
        let kind = ParseErrorKind::Expecting {
66
            value: "filename".to_string(),
67
        };
68
        return Err(ParseError::new(start.pos, false, kind));
69
    }
70

            
71
872
    let end = reader.cursor();
72
872
    let template = Template::new(None, elements, SourceInfo::new(start.pos, end.pos));
73
872
    Ok(template)
74
}
75

            
76
1692
fn filename_content(reader: &mut Reader) -> ParseResult<String> {
77
1692
    let mut s = String::new();
78
    loop {
79
2536
        match filename_escaped_char(reader) {
80
16
            Ok(c) => {
81
16
                s.push(c);
82
            }
83
2520
            Err(e) => {
84
2520
                if e.recoverable {
85
2520
                    let s2 = filename_text(reader);
86
2520
                    if s2.is_empty() {
87
1692
                        break;
88
828
                    } else {
89
828
                        s.push_str(&s2);
90
                    }
91
                } else {
92
                    return Err(e);
93
                }
94
            }
95
        }
96
    }
97
1692
    Ok(s)
98
}
99

            
100
2520
fn filename_text(reader: &mut Reader) -> String {
101
2520
    let mut s = String::new();
102
    loop {
103
11456
        let save = reader.cursor();
104
11456
        match reader.read() {
105
4
            None => break,
106
11452
            Some(c) => {
107
11452
                if ['#', ';', '{', '}', ' ', '\n', '\r', '\\'].contains(&c) {
108
2516
                    reader.seek(save);
109
2516
                    break;
110
8936
                } else {
111
8936
                    s.push(c);
112
                }
113
            }
114
        }
115
    }
116
2520
    s
117
}
118

            
119
2536
fn filename_escaped_char(reader: &mut Reader) -> ParseResult<char> {
120
2536
    try_literal("\\", reader)?;
121
16
    let start = reader.cursor();
122
16
    match reader.read() {
123
        Some('\\') => Ok('\\'),
124
        Some('b') => Ok('\x08'),
125
        Some('f') => Ok('\x0c'),
126
        Some('n') => Ok('\n'),
127
        Some('r') => Ok('\r'),
128
        Some('t') => Ok('\t'),
129
        Some('#') => Ok('#'),
130
        Some(';') => Ok(';'),
131
16
        Some(' ') => Ok(' '),
132
        Some('{') => Ok('{'),
133
        Some('}') => Ok('}'),
134
        Some('u') => string::unicode(reader),
135
        _ => Err(ParseError::new(
136
            start.pos,
137
            false,
138
            ParseErrorKind::EscapeChar,
139
        )),
140
    }
141
}
142

            
143
#[cfg(test)]
144
mod tests {
145
    use super::*;
146
    use crate::ast::{Expr, ExprKind, Placeholder, Variable, Whitespace};
147
    use crate::reader::{CharPos, Pos};
148

            
149
    #[test]
150
    fn test_filename() {
151
        let mut reader = Reader::new("data/data.bin");
152
        assert_eq!(
153
            parse(&mut reader).unwrap(),
154
            Template::new(
155
                None,
156
                vec![TemplateElement::String {
157
                    value: "data/data.bin".to_string(),
158
                    source: "data/data.bin".to_source()
159
                }],
160
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 14)),
161
            )
162
        );
163
        assert_eq!(reader.cursor().index, CharPos(13));
164

            
165
        let mut reader = Reader::new("data.bin");
166
        assert_eq!(
167
            parse(&mut reader).unwrap(),
168
            Template::new(
169
                None,
170
                vec![TemplateElement::String {
171
                    value: "data.bin".to_string(),
172
                    source: "data.bin".to_source()
173
                }],
174
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 9)),
175
            )
176
        );
177
        assert_eq!(reader.cursor().index, CharPos(8));
178
    }
179

            
180
    #[test]
181
    fn test_include_space() {
182
        let mut reader = Reader::new("file\\ with\\ spaces");
183
        assert_eq!(
184
            parse(&mut reader).unwrap(),
185
            Template::new(
186
                None,
187
                vec![TemplateElement::String {
188
                    value: "file with spaces".to_string(),
189
                    source: "file\\ with\\ spaces".to_source()
190
                }],
191
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 19)),
192
            )
193
        );
194
        assert_eq!(reader.cursor().index, CharPos(18));
195
    }
196

            
197
    #[test]
198
    fn test_escaped_chars() {
199
        let mut reader = Reader::new("filename\\{"); // to the possible escaped chars
200
        assert_eq!(
201
            parse(&mut reader).unwrap(),
202
            Template::new(
203
                None,
204
                vec![TemplateElement::String {
205
                    value: "filename{".to_string(),
206
                    source: "filename\\{".to_source()
207
                }],
208
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 11))
209
            )
210
        );
211
        assert_eq!(reader.cursor().index, CharPos(10));
212
    }
213

            
214
    #[test]
215
    fn test_filename_error() {
216
        let mut reader = Reader::new("{");
217
        let error = parse(&mut reader).err().unwrap();
218
        assert_eq!(error.kind, ParseErrorKind::Filename);
219
        assert_eq!(error.pos, Pos { line: 1, column: 1 });
220

            
221
        let mut reader = Reader::new("\\:");
222
        let error = parse(&mut reader).err().unwrap();
223
        assert_eq!(error.kind, ParseErrorKind::EscapeChar);
224
        assert_eq!(error.pos, Pos { line: 1, column: 2 });
225
    }
226

            
227
    #[test]
228
    fn test_filename_with_variables() {
229
        let mut reader = Reader::new("foo_{{bar}}");
230
        assert_eq!(
231
            parse(&mut reader).unwrap(),
232
            Template::new(
233
                None,
234
                vec![
235
                    TemplateElement::String {
236
                        value: "foo_".to_string(),
237
                        source: "foo_".to_source(),
238
                    },
239
                    TemplateElement::Placeholder(Placeholder {
240
                        space0: Whitespace {
241
                            value: String::new(),
242
                            source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 7)),
243
                        },
244
                        expr: Expr {
245
                            kind: ExprKind::Variable(Variable {
246
                                name: "bar".to_string(),
247
                                source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 10)),
248
                            }),
249
                            source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 10)),
250
                        },
251
                        space1: Whitespace {
252
                            value: String::new(),
253
                            source_info: SourceInfo::new(Pos::new(1, 10), Pos::new(1, 10)),
254
                        },
255
                    })
256
                ],
257
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 12)),
258
            )
259
        );
260

            
261
        let mut reader = Reader::new("foo_{{bar}}_baz");
262
        assert_eq!(
263
            parse(&mut reader).unwrap(),
264
            Template::new(
265
                None,
266
                vec![
267
                    TemplateElement::String {
268
                        value: "foo_".to_string(),
269
                        source: "foo_".to_source(),
270
                    },
271
                    TemplateElement::Placeholder(Placeholder {
272
                        space0: Whitespace {
273
                            value: String::new(),
274
                            source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 7)),
275
                        },
276
                        expr: Expr {
277
                            kind: ExprKind::Variable(Variable {
278
                                name: "bar".to_string(),
279
                                source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 10)),
280
                            }),
281
                            source_info: SourceInfo::new(Pos::new(1, 7), Pos::new(1, 10)),
282
                        },
283
                        space1: Whitespace {
284
                            value: String::new(),
285
                            source_info: SourceInfo::new(Pos::new(1, 10), Pos::new(1, 10)),
286
                        },
287
                    }),
288
                    TemplateElement::String {
289
                        value: "_baz".to_string(),
290
                        source: "_baz".to_source(),
291
                    },
292
                ],
293
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 16)),
294
            )
295
        );
296
    }
297

            
298
    #[test]
299
    fn test_filename_with_crlf() {
300
        let mut reader = Reader::new("foo\r\n");
301
        assert_eq!(
302
            parse(&mut reader).unwrap(),
303
            Template::new(
304
                None,
305
                vec![TemplateElement::String {
306
                    value: "foo".to_string(),
307
                    source: "foo".to_source(),
308
                }],
309
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 4)),
310
            )
311
        );
312
    }
313
}