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 std::os::raw::{c_char, c_int, c_void};
19
use std::ptr;
20

            
21
use libxml::bindings::{
22
    xmlChar, xmlCreatePushParserCtxt, xmlFreeParserCtxt, xmlParseChunk, xmlSAXHandlerPtr,
23
};
24

            
25
use crate::reader::Reader;
26

            
27
use super::ParseResult;
28
use super::error::{ParseError, ParseErrorKind};
29

            
30
/// Parses a text buffer until a valid XML has been found.
31
/// We're using a SAX XML parser because we need to stop the parsing at the byte position where
32
/// an XML text is detected.
33
/// For example, when we have this kind of Hurl file:
34
///
35
/// ```hurl
36
/// POST https://foo.com
37
/// <?xml version="1.0"?>
38
/// <catalog>
39
///   <book id="bk101">
40
///     <author>Gambardella, Matthew</author>
41
///     <title>XML Developer's Guide</title>
42
///   </book>
43
/// </catalog>
44
/// HTTP 201
45
/// ```
46
///
47
/// As there is no "formal" end of body, we need to parse the string until we detect at the precise
48
/// byte a possible valid XML body.
49
///
50
23788
pub fn parse(reader: &mut Reader) -> ParseResult<String> {
51
    // We test if our first character is a start of an XML text.
52
    // If not, we return immediately a recoverable error.
53
    // Otherwise, we start parsing the supposed XML buffer. Any subsequent error will be a
54
    // non-recoverable error.
55
23788
    let c = reader.peek();
56
23788
    match c {
57
92
        Some('<') => {}
58
        _ => {
59
23696
            return Err(ParseError::new(
60
23696
                reader.cursor().pos,
61
23696
                true,
62
23696
                ParseErrorKind::Xml,
63
23696
            ));
64
        }
65
    }
66

            
67
92
    let mut buf = String::new();
68
92
    let mut parser = new_sax_parser();
69
92
    let mut parser_context = ParserContext::new();
70

            
71
    // We use libxml SAX parser to identify the end of the XML body.
72
    // We feed the SAX parser chars by chars (Rust char), so chunks are UFT-8 bytes,
73
    // 1 byte to 4 bytes long. The detection of the body end is done when receiving a closing
74
    // element event by testing the depth of the XML tree.
75
    unsafe {
76
92
        let context = xmlCreatePushParserCtxt(
77
92
            &mut parser as xmlSAXHandlerPtr,
78
92
            &mut parser_context as *mut ParserContext as *mut c_void,
79
92
            ptr::null(),
80
            0,
81
92
            ptr::null(),
82
        );
83

            
84
        // We keep track of the previous char reader position, to accurately raise eventual error.
85
92
        let mut prev_pos = reader.cursor().pos;
86

            
87
357388
        while let Some(c) = reader.read() {
88
357388
            buf.push(c);
89

            
90
            // We feed the parser chars by chars.
91
            // A buffer of length four is large enough to encode any char.
92
357388
            let mut bytes = [0u8; 4];
93
357388
            let end = reader.is_eof() as c_int;
94
357388
            let bytes = c.encode_utf8(&mut bytes);
95
357388
            let count = bytes.len() as c_int;
96
357388
            let bytes = bytes.as_ptr() as *const c_char;
97
357388
            let ret = xmlParseChunk(context, bytes, count, end);
98
357388
            if ret != 0 {
99
4
                xmlFreeParserCtxt(context);
100
4
                return Err(ParseError::new(prev_pos, false, ParseErrorKind::Xml));
101
            }
102

            
103
            // End of the XML body is detected with a closing element event and depth of the tree.
104
            // There is also a closing document event but it's not always raised at the exact
105
            // closing `>` position.
106
357384
            if std::matches!(parser_context.state, ParserState::EndElement)
107
127224
                && parser_context.depth == 0
108
            {
109
88
                break;
110
            }
111
357296
            prev_pos = reader.cursor().pos;
112
        }
113

            
114
88
        xmlFreeParserCtxt(context);
115
    }
116

            
117
88
    Ok(buf)
118
}
119

            
120
/// A context for the SAX parser, containing a `state` and the current tree `depth`.
121
struct ParserContext {
122
    depth: usize,
123
    state: ParserState,
124
}
125

            
126
impl ParserContext {
127
92
    fn new() -> ParserContext {
128
92
        ParserContext {
129
92
            depth: 0,
130
92
            state: ParserState::Created,
131
        }
132
    }
133
}
134

            
135
enum ParserState {
136
    Created,
137
    StartDocument,
138
    EndDocument,
139
    StartElement,
140
    EndElement,
141
}
142

            
143
92
fn new_sax_parser() -> libxml::bindings::xmlSAXHandler {
144
92
    libxml::bindings::xmlSAXHandler {
145
92
        internalSubset: None,
146
92
        isStandalone: None,
147
92
        hasInternalSubset: None,
148
92
        hasExternalSubset: None,
149
92
        resolveEntity: None,
150
92
        getEntity: None,
151
92
        entityDecl: None,
152
92
        notationDecl: None,
153
92
        attributeDecl: None,
154
92
        elementDecl: None,
155
92
        unparsedEntityDecl: None,
156
92
        setDocumentLocator: None,
157
92
        startDocument: Some(on_start_document),
158
92
        endDocument: Some(on_end_document),
159
92
        startElement: None,
160
92
        endElement: None,
161
92
        reference: None,
162
92
        characters: None,
163
92
        ignorableWhitespace: None,
164
92
        processingInstruction: None,
165
92
        comment: None,
166
92
        warning: None,
167
92
        error: None,
168
92
        fatalError: None,
169
92
        getParameterEntity: None,
170
92
        cdataBlock: None,
171
92
        externalSubset: None,
172
92
        initialized: libxml::bindings::XML_SAX2_MAGIC,
173
92
        _private: ptr::null_mut(),
174
92
        startElementNs: Some(on_start_element),
175
92
        endElementNs: Some(on_end_element),
176
92
        serror: None,
177
    }
178
}
179

            
180
/// Called when the document start being processed.
181
92
unsafe extern "C" fn on_start_document(ctx: *mut c_void) {
182
92
    let context: &mut ParserContext = unsafe { &mut *(ctx as *mut ParserContext) };
183
92
    context.state = ParserState::StartDocument;
184
}
185

            
186
/// Called when the document end has been detected.
187
4
unsafe extern "C" fn on_end_document(ctx: *mut c_void) {
188
4
    let context: &mut ParserContext = unsafe { &mut *(ctx as *mut ParserContext) };
189
4
    context.state = ParserState::EndDocument;
190
}
191

            
192
/// Called when an opening tag has been processed.
193
5584
unsafe extern "C" fn on_start_element(
194
5584
    ctx: *mut c_void,
195
5584
    _local_name: *const xmlChar,
196
5584
    _prefix: *const xmlChar,
197
5584
    _uri: *const xmlChar,
198
5584
    _nb_namespaces: c_int,
199
5584
    _namespaces: *mut *const xmlChar,
200
5584
    _nb_attributes: c_int,
201
5584
    _nb_defaulted: c_int,
202
5584
    _attributes: *mut *const xmlChar,
203
5584
) {
204
5584
    let context: &mut ParserContext = unsafe { &mut *(ctx as *mut ParserContext) };
205
5584
    context.state = ParserState::StartElement;
206
5584
    context.depth += 1;
207
}
208

            
209
/// Called when the end of an element has been detected.
210
5576
unsafe extern "C" fn on_end_element(
211
5576
    ctx: *mut c_void,
212
5576
    _local_name: *const xmlChar,
213
5576
    _prefix: *const xmlChar,
214
5576
    _uri: *const xmlChar,
215
5576
) {
216
5576
    let context: &mut ParserContext = unsafe { &mut *(ctx as *mut ParserContext) };
217
5576
    context.state = ParserState::EndElement;
218
5576
    context.depth -= 1;
219
}
220

            
221
#[cfg(test)]
222
mod tests {
223
    use super::*;
224
    use crate::reader::{CharPos, Pos};
225

            
226
    #[test]
227
    fn parse_xml_brute_force_errors() {
228
        let mut reader = Reader::new("");
229
        let error = parse(&mut reader).err().unwrap();
230
        assert_eq!(error.pos, Pos { line: 1, column: 1 });
231
        assert_eq!(error.kind, ParseErrorKind::Xml);
232
        assert!(error.recoverable);
233

            
234
        let mut reader = Reader::new("x");
235
        let error = parse(&mut reader).err().unwrap();
236
        assert_eq!(error.pos, Pos { line: 1, column: 1 });
237
        assert_eq!(error.kind, ParseErrorKind::Xml);
238
        assert!(error.recoverable);
239

            
240
        let mut reader = Reader::new("<<");
241
        let error = parse(&mut reader).err().unwrap();
242
        assert_eq!(error.pos, Pos { line: 1, column: 2 });
243
        assert_eq!(error.kind, ParseErrorKind::Xml);
244
        assert!(!error.recoverable);
245

            
246
        let mut reader = Reader::new("<users><user /></users");
247
        let error = parse(&mut reader).err().unwrap();
248
        assert_eq!(
249
            error.pos,
250
            Pos {
251
                line: 1,
252
                column: 22
253
            }
254
        );
255
        assert_eq!(error.kind, ParseErrorKind::Xml);
256

            
257
        let mut reader = Reader::new("<users aa><user /></users");
258
        let error = parse(&mut reader).err().unwrap();
259
        assert_eq!(
260
            error.pos,
261
            Pos {
262
                line: 1,
263
                column: 10
264
            }
265
        );
266
        assert_eq!(error.kind, ParseErrorKind::Xml);
267
    }
268

            
269
    #[test]
270
    fn parse_xml_brute_force_ok() {
271
        let mut reader = Reader::new("<users><user /></users>");
272
        assert_eq!(
273
            parse(&mut reader).unwrap(),
274
            String::from("<users><user /></users>")
275
        );
276
        assert_eq!(reader.cursor().index, CharPos(23));
277

            
278
        let mut reader = Reader::new("<users><user /></users>xx");
279
        assert_eq!(
280
            parse(&mut reader).unwrap(),
281
            String::from("<users><user /></users>")
282
        );
283
        assert_eq!(reader.cursor().index, CharPos(23));
284
        assert_eq!(reader.peek_n(2), String::from("xx"));
285

            
286
        let mut reader = Reader::new("<?xml version=\"1.0\"?><users/>xxx");
287
        assert_eq!(
288
            parse(&mut reader).unwrap(),
289
            String::from("<?xml version=\"1.0\"?><users/>")
290
        );
291
        assert_eq!(reader.cursor().index, CharPos(29));
292
    }
293

            
294
    #[test]
295
    fn parse_xml_soap_utf8() {
296
        let xml = r#"<?xml version='1.0' encoding='UTF-8'?>
297
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
298
    <soapenv:Body>
299
        <ns31:UploadInboundResponseElement xmlns:ns31="http://www.example.com/schema/xyzWS">
300
            <ns31:UploadInboundResult>&lt;?xml version="1.0" encoding="UTF-8" ?>&lt;ATKCST>&lt;Head>&lt;FunCode>9000&lt;/FunCode>&lt;Remark>接收数据成功&lt;/Remark>&lt;/Head>&lt;/ATKCST></ns31:UploadInboundResult>
301
        </ns31:UploadInboundResponseElement>
302
    </soapenv:Body>
303
</soapenv:Envelope>"#;
304

            
305
        // A valid complete XML
306
        let input = xml;
307
        let output = xml;
308
        let mut reader = Reader::new(input);
309
        assert_eq!(parse(&mut reader).unwrap(), String::from(output),);
310
        assert_eq!(reader.cursor().index, CharPos(520));
311

            
312
        // A XML with data padding
313
        let input = format!("{xml} xx xx xx xx");
314
        let output = xml;
315
        let mut reader = Reader::new(&input);
316
        assert_eq!(parse(&mut reader).unwrap(), String::from(output),);
317
        assert_eq!(reader.cursor().index, CharPos(520));
318

            
319
        // Two consecutive XML
320
        let input = format!("{xml}{xml}");
321
        let output = xml;
322
        let mut reader = Reader::new(&input);
323
        assert_eq!(parse(&mut reader).unwrap(), String::from(output),);
324
        assert_eq!(reader.cursor().index, CharPos(520));
325

            
326
        let mut reader = Reader::new(&input);
327
        assert_eq!(parse(&mut reader).unwrap(), String::from(output),);
328
        assert_eq!(reader.cursor().index, CharPos(520));
329
    }
330

            
331
    #[test]
332
    fn parse_xml_books_with_entry_response_start() {
333
        let xml = r#"<?xml version="1.0"?>
334
<catalog>
335
   <book id="bk101">
336
      <author>Gambardella, Matthew</author>
337
      <title>XML Developer's Guide</title>
338
      <genre>Computer</genre>
339
      <price>44.95</price>
340
      <publish_date>2000-10-01</publish_date>
341
      <description>An in-depth look at creating applications
342
      with XML.</description>
343
   </book>
344
   <book id="bk102">
345
      <author>Ralls, Kim</author>
346
      <title>Midnight Rain</title>
347
      <genre>Fantasy</genre>
348
      <price>5.95</price>
349
      <publish_date>2000-12-16</publish_date>
350
      <description>A former architect battles corporate zombies,
351
      an evil sorceress, and her own childhood to become queen
352
      of the world.</description>
353
   </book>
354
   <book id="bk103">
355
      <author>Corets, Eva</author>
356
      <title>Maeve Ascendant</title>
357
      <genre>Fantasy</genre>
358
      <price>5.95</price>
359
      <publish_date>2000-11-17</publish_date>
360
      <description>After the collapse of a nanotechnology
361
      society in England, the young survivors lay the
362
      foundation for a new society.</description>
363
   </book>
364
   <book id="bk104">
365
      <author>Corets, Eva</author>
366
      <title>Oberon's Legacy</title>
367
      <genre>Fantasy</genre>
368
      <price>5.95</price>
369
      <publish_date>2001-03-10</publish_date>
370
      <description>In post-apocalypse England, the mysterious
371
      agent known only as Oberon helps to create a new life
372
      for the inhabitants of London. Sequel to Maeve
373
      Ascendant.</description>
374
   </book>
375
   <book id="bk105">
376
      <author>Corets, Eva</author>
377
      <title>The Sundered Grail</title>
378
      <genre>Fantasy</genre>
379
      <price>5.95</price>
380
      <publish_date>2001-09-10</publish_date>
381
      <description>The two daughters of Maeve, half-sisters,
382
      battle one another for control of England. Sequel to
383
      Oberon's Legacy.</description>
384
   </book>
385
   <book id="bk106">
386
      <author>Randall, Cynthia</author>
387
      <title>Lover Birds</title>
388
      <genre>Romance</genre>
389
      <price>4.95</price>
390
      <publish_date>2000-09-02</publish_date>
391
      <description>When Carla meets Paul at an ornithology
392
      conference, tempers fly as feathers get ruffled.</description>
393
   </book>
394
   <book id="bk107">
395
      <author>Thurman, Paula</author>
396
      <title>Splish Splash</title>
397
      <genre>Romance</genre>
398
      <price>4.95</price>
399
      <publish_date>2000-11-02</publish_date>
400
      <description>A deep sea diver finds true love twenty
401
      thousand leagues beneath the sea.</description>
402
   </book>
403
   <book id="bk108">
404
      <author>Knorr, Stefan</author>
405
      <title>Creepy Crawlies</title>
406
      <genre>Horror</genre>
407
      <price>4.95</price>
408
      <publish_date>2000-12-06</publish_date>
409
      <description>An anthology of horror stories about roaches,
410
      centipedes, scorpions  and other insects.</description>
411
   </book>
412
   <book id="bk109">
413
      <author>Kress, Peter</author>
414
      <title>Paradox Lost</title>
415
      <genre>Science Fiction</genre>
416
      <price>6.95</price>
417
      <publish_date>2000-11-02</publish_date>
418
      <description>After an inadvertent trip through a Heisenberg
419
      Uncertainty Device, James Salway discovers the problems
420
      of being quantum.</description>
421
   </book>
422
   <book id="bk110">
423
      <author>O'Brien, Tim</author>
424
      <title>Microsoft .NET: The Programming Bible</title>
425
      <genre>Computer</genre>
426
      <price>36.95</price>
427
      <publish_date>2000-12-09</publish_date>
428
      <description>Microsoft's .NET initiative is explored in
429
      detail in this deep programmer's reference.</description>
430
   </book>
431
   <book id="bk111">
432
      <author>O'Brien, Tim</author>
433
      <title>MSXML3: A Comprehensive Guide</title>
434
      <genre>Computer</genre>
435
      <price>36.95</price>
436
      <publish_date>2000-12-01</publish_date>
437
      <description>The Microsoft MSXML3 parser is covered in
438
      detail, with attention to XML DOM interfaces, XSLT processing,
439
      SAX and more.</description>
440
   </book>
441
   <book id="bk112">
442
      <author>Galos, Mike</author>
443
      <title>Visual Studio 7: A Comprehensive Guide</title>
444
      <genre>Computer</genre>
445
      <price>49.95</price>
446
      <publish_date>2001-04-16</publish_date>
447
      <description>Microsoft Visual Studio 7 is explored in depth,
448
      looking at how Visual Basic, Visual C++, C#, and ASP+ are
449
      integrated into a comprehensive development
450
      environment.</description>
451
   </book>
452
</catalog>"#;
453

            
454
        let chunk = format!("{xml}\nHTTP 200");
455
        let mut reader = Reader::new(&chunk);
456
        assert_eq!(parse(&mut reader).unwrap(), String::from(xml),);
457
        assert_eq!(reader.cursor().index, CharPos(4411));
458
    }
459
}