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 super::placeholder;
19
use crate::ast::{SourceInfo, Template, TemplateElement};
20
use crate::parser::primitives::try_literal;
21
use crate::parser::{string, ParseError, ParseErrorKind, ParseResult};
22
use crate::reader::Reader;
23
use crate::typing::ToSource;
24

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

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

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

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

            
99
2625
fn filename_text(reader: &mut Reader) -> String {
100
2625
    let mut s = String::new();
101
    loop {
102
11945
        let save = reader.cursor();
103
11945
        match reader.read() {
104
5
            None => break,
105
11940
            Some(c) => {
106
11940
                if ['#', ';', '{', '}', ' ', '\n', '\\'].contains(&c) {
107
2620
                    reader.seek(save);
108
2620
                    break;
109
9320
                } else {
110
9320
                    s.push(c);
111
                }
112
            }
113
        }
114
    }
115
2625
    s
116
}
117

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

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

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

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

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

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

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

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

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

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