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 with an optional password
27
///
28
/// This is very similar to the filename parser.
29
/// There is still a small different due to the password delimiter ":"
30
/// If the ':' character is part of the filename, it must be escaped.
31
/// While in the standard filename parser, you can not escape this character at all.
32
///
33
64
pub fn parse(reader: &mut Reader) -> ParseResult<Template> {
34
64
    let start = reader.cursor();
35

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

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

            
77
108
fn filename_password_content(reader: &mut Reader) -> ParseResult<String> {
78
108
    let mut s = String::new();
79
    loop {
80
152
        match filename_password_escaped_char(reader) {
81
            Ok(c) => {
82
                // ':' is escaped so that is it not recognized as the password delimiter
83
                // This is not due to Hurl format
84
                if c == ':' {
85
                    s.push('\\');
86
                }
87
                s.push(c);
88
            }
89
152
            Err(e) => {
90
152
                if e.recoverable {
91
152
                    let s2 = filename_password_text(reader);
92
152
                    if s2.is_empty() {
93
108
                        break;
94
44
                    } else {
95
44
                        s.push_str(&s2);
96
                    }
97
                } else {
98
                    return Err(e);
99
                }
100
            }
101
        }
102
    }
103
108
    Ok(s)
104
}
105

            
106
152
fn filename_password_text(reader: &mut Reader) -> String {
107
152
    let mut s = String::new();
108
    loop {
109
932
        let save = reader.cursor();
110
932
        match reader.read() {
111
            None => break,
112
932
            Some(c) => {
113
932
                if ['#', ';', '{', '}', ' ', '\n', '\r', '\\'].contains(&c) {
114
152
                    reader.seek(save);
115
152
                    break;
116
780
                } else {
117
780
                    s.push(c);
118
                }
119
            }
120
        }
121
    }
122
152
    s
123
}
124

            
125
152
fn filename_password_escaped_char(reader: &mut Reader) -> ParseResult<char> {
126
152
    try_literal("\\", reader)?;
127
    let start = reader.cursor();
128
    match reader.read() {
129
        Some('\\') => Ok('\\'),
130
        Some('b') => Ok('\x08'),
131
        Some('f') => Ok('\x0c'),
132
        Some('n') => Ok('\n'),
133
        Some('r') => Ok('\r'),
134
        Some('t') => Ok('\t'),
135
        Some('#') => Ok('#'),
136
        Some(';') => Ok(';'),
137
        Some(' ') => Ok(' '),
138
        Some('{') => Ok('{'),
139
        Some('}') => Ok('}'),
140
        Some(':') => Ok(':'),
141
        Some('u') => string::unicode(reader),
142
        _ => Err(ParseError::new(
143
            start.pos,
144
            false,
145
            ParseErrorKind::EscapeChar,
146
        )),
147
    }
148
}
149

            
150
#[cfg(test)]
151
mod tests {
152
    use super::*;
153
    use crate::reader::{CharPos, Pos};
154

            
155
    #[test]
156
    fn test_filename_with_password() {
157
        let mut reader = Reader::new("file\\:123:pwd\\:\\#:");
158
        assert_eq!(
159
            parse(&mut reader).unwrap(),
160
            Template::new(
161
                None,
162
                vec![TemplateElement::String {
163
                    value: "file\\:123:pwd\\:#:".to_string(),
164
                    source: "file\\:123:pwd\\:\\#:".to_source()
165
                }],
166
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 19)),
167
            )
168
        );
169
        assert_eq!(reader.cursor().index, CharPos(18));
170
    }
171

            
172
    #[test]
173
    fn test_parse() {
174
        let mut reader = Reader::new("/etc/client-cert.pem #foo");
175
        assert_eq!(
176
            parse(&mut reader).unwrap(),
177
            Template::new(
178
                None,
179
                vec![TemplateElement::String {
180
                    value: "/etc/client-cert.pem".to_string(),
181
                    source: "/etc/client-cert.pem".to_source()
182
                }],
183
                SourceInfo::new(Pos::new(1, 1), Pos::new(1, 21)),
184
            )
185
        );
186
        assert_eq!(reader.cursor().index, CharPos(20));
187
    }
188
}