1
29
//! Main SVG document structure.
2

            
3
use data_url::mime::Mime;
4
use gdk_pixbuf::{prelude::PixbufLoaderExt, PixbufLoader};
5
use markup5ever::QualName;
6
use once_cell::sync::Lazy;
7
use std::cell::RefCell;
8
use std::collections::hash_map::Entry;
9
use std::collections::HashMap;
10
use std::fmt;
11
use std::include_str;
12
use std::rc::Rc;
13
use std::str::FromStr;
14
use std::sync::Arc;
15

            
16
use crate::css::{self, Origin, Stylesheet};
17
use crate::error::{AcquireError, LoadingError, NodeIdError};
18
use crate::handle::LoadOptions;
19
8
use crate::io::{self, BinaryData};
20
8
use crate::limits;
21
8
use crate::node::{Node, NodeBorrow, NodeData};
22
use crate::session::Session;
23
use crate::surface_utils::shared_surface::SharedImageSurface;
24
use crate::url_resolver::{AllowedUrl, UrlResolver};
25
use crate::xml::{xml_load_from_possibly_compressed_stream, Attributes};
26

            
27
85
static UA_STYLESHEETS: Lazy<Vec<Stylesheet>> = Lazy::new(|| {
28
170
    vec![Stylesheet::from_data(
29
        include_str!("ua.css"),
30
85
        &UrlResolver::new(None),
31
85
        Origin::UserAgent,
32
85
        Session::default(),
33
    )
34
24
    .expect("could not parse user agent stylesheet for librsvg, there's a bug!")]
35
109
});
36
24

            
37
/// Identifier of a node
38
505827
#[derive(Debug, PartialEq, Clone)]
39
336
pub enum NodeId {
40
336
    /// element id
41
505785
    Internal(String),
42
    /// url, element id
43
352
    External(String, String),
44
}
45

            
46
impl NodeId {
47
1512
    pub fn parse(href: &str) -> Result<NodeId, NodeIdError> {
48
3024
        let (url, id) = match href.rfind('#') {
49
47
            None => (Some(href), None),
50
1465
            Some(p) if p == 0 => (None, Some(&href[1..])),
51
20
            Some(p) => (Some(&href[..p]), Some(&href[(p + 1)..])),
52
        };
53
23

            
54
1535
        match (url, id) {
55
1468
            (None, Some(id)) if !id.is_empty() => Ok(NodeId::Internal(String::from(id))),
56
43
            (Some(url), Some(id)) if !id.is_empty() => {
57
43
                Ok(NodeId::External(String::from(url), String::from(id)))
58
43
            }
59
47
            _ => Err(NodeIdError::NodeIdRequired),
60
        }
61
1535
    }
62
23
}
63
8

            
64
8
impl fmt::Display for NodeId {
65
31
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66
8
        match self {
67
8
            NodeId::Internal(id) => write!(f, "#{id}"),
68
            NodeId::External(url, id) => write!(f, "{url}#{id}"),
69
        }
70
    }
71
8
}
72
8

            
73
8
/// A loaded SVG file and its derived data.
74
8
pub struct Document {
75
8
    /// Tree of nodes; the root is guaranteed to be an `<svg>` element.
76
8
    tree: Node,
77

            
78
8
    /// Metadata about the SVG handle.
79
    session: Session,
80

            
81
    /// Mapping from `id` attributes to nodes.
82
    ids: HashMap<String, Node>,
83

            
84
    // The following two require interior mutability because we load the extern
85
8
    // resources all over the place.  Eventually we'll be able to do this
86
8
    // once, at loading time, and keep this immutable.
87
8
    /// SVG documents referenced from this document.
88
8
    externs: RefCell<Resources>,
89
8

            
90
    /// Image resources referenced from this document.
91
    images: RefCell<Images>,
92

            
93
    /// Used to load referenced resources.
94
    load_options: Arc<LoadOptions>,
95
24

            
96
24
    /// Stylesheets defined in the document.
97
24
    stylesheets: Vec<Stylesheet>,
98
}
99

            
100
impl Document {
101
    /// Constructs a `Document` by loading it from a stream.
102
1072
    pub fn load_from_stream(
103
        session: Session,
104
        load_options: Arc<LoadOptions>,
105
        stream: &gio::InputStream,
106
        cancellable: Option<&gio::Cancellable>,
107
    ) -> Result<Document, LoadingError> {
108
1072
        xml_load_from_possibly_compressed_stream(
109
1072
            session.clone(),
110
1072
            DocumentBuilder::new(session, load_options.clone()),
111
1072
            load_options,
112
            stream,
113
            cancellable,
114
1072
        )
115
1072
    }
116

            
117
    /// Utility function to load a document from a static string in tests.
118
    #[cfg(test)]
119
5
    pub fn load_from_bytes(input: &'static [u8]) -> Document {
120
        use glib::prelude::*;
121

            
122
5
        let bytes = glib::Bytes::from_static(input);
123
5
        let stream = gio::MemoryInputStream::from_bytes(&bytes);
124

            
125
5
        Document::load_from_stream(
126
5
            Session::new_for_test_suite(),
127
5
            Arc::new(LoadOptions::new(UrlResolver::new(None))),
128
5
            &stream.upcast(),
129
5
            None::<&gio::Cancellable>,
130
162
        )
131
162
        .unwrap()
132
167
    }
133

            
134
162
    /// Gets the root node.  This is guaranteed to be an `<svg>` element.
135
4273
    pub fn root(&self) -> Node {
136
4273
        self.tree.clone()
137
4273
    }
138

            
139
    /// Looks up a node in this document or one of its resources by its `id` attribute.
140
1001014
    pub fn lookup_node(&self, node_id: &NodeId) -> Option<Node> {
141
1001014
        match node_id {
142
1001003
            NodeId::Internal(id) => self.lookup_internal_node(id),
143
22
            NodeId::External(url, id) => self
144
162
                .externs
145
162
                .borrow_mut()
146
11
                .lookup(&self.session, &self.load_options, url, id)
147
11
                .ok(),
148
162
        }
149
1001176
    }
150
324

            
151
162
    /// Looks up a node in this document by its `id` attribute.
152
1001074
    pub fn lookup_internal_node(&self, id: &str) -> Option<Node> {
153
2002052
        self.ids.get(id).map(|n| (*n).clone())
154
1001074
    }
155

            
156
    /// Loads an image by URL, or returns a pre-loaded one.
157
136
    pub fn lookup_image(&self, url: &str) -> Result<SharedImageSurface, LoadingError> {
158
136
        let aurl = self
159
            .load_options
160
            .url_resolver
161
            .resolve_href(url)
162
2
            .map_err(|_| LoadingError::BadUrl)?;
163

            
164
135
        self.images.borrow_mut().lookup(&self.load_options, &aurl)
165
136
    }
166

            
167
    /// Runs the CSS cascade on the document tree
168
    ///
169
    /// This uses the default UserAgent stylesheet, the document's internal stylesheets,
170
    /// plus an extra set of stylesheets supplied by the caller.
171
1062
    pub fn cascade(&mut self, extra: &[Stylesheet], session: &Session) {
172
1062
        css::cascade(
173
1062
            &mut self.tree,
174
1062
            &UA_STYLESHEETS,
175
1062
            &self.stylesheets,
176
            extra,
177
            session,
178
        );
179
1062
    }
180
}
181

            
182
struct Resources {
183
    resources: HashMap<AllowedUrl, Result<Rc<Document>, LoadingError>>,
184
}
185

            
186
impl Resources {
187
1058
    pub fn new() -> Resources {
188
1058
        Resources {
189
1058
            resources: Default::default(),
190
133
        }
191
1191
    }
192
133

            
193
11
    pub fn lookup(
194
        &mut self,
195
        session: &Session,
196
        load_options: &LoadOptions,
197
        url: &str,
198
        id: &str,
199
    ) -> Result<Node, LoadingError> {
200
22
        self.get_extern_document(session, load_options, url)
201
18
            .and_then(|doc| doc.lookup_internal_node(id).ok_or(LoadingError::BadUrl))
202
11
    }
203

            
204
11
    fn get_extern_document(
205
        &mut self,
206
        session: &Session,
207
        load_options: &LoadOptions,
208
        href: &str,
209
    ) -> Result<Rc<Document>, LoadingError> {
210
11
        let aurl = load_options
211
            .url_resolver
212
            .resolve_href(href)
213
8
            .map_err(|_| LoadingError::BadUrl)?;
214

            
215
7
        match self.resources.entry(aurl) {
216
4
            Entry::Occupied(e) => e.get().clone(),
217
3
            Entry::Vacant(e) => {
218
3
                let aurl = e.key();
219
                // FIXME: pass a cancellable to these
220
3
                let doc = io::acquire_stream(aurl, None)
221
                    .map_err(LoadingError::from)
222
6
                    .and_then(|stream| {
223
3
                        Document::load_from_stream(
224
3
                            session.clone(),
225
3
                            Arc::new(load_options.copy_with_base_url(aurl)),
226
                            &stream,
227
3
                            None,
228
3
                        )
229
3
                    })
230
                    .map(Rc::new);
231
3
                let res = e.insert(doc);
232
3
                res.clone()
233
3
            }
234
        }
235
11
    }
236
}
237

            
238
31
struct Images {
239
62
    images: HashMap<AllowedUrl, Result<SharedImageSurface, LoadingError>>,
240
}
241

            
242
62
impl Images {
243
1089
    fn new() -> Images {
244
1058
        Images {
245
1058
            images: Default::default(),
246
        }
247
1058
    }
248

            
249
135
    fn lookup(
250
        &mut self,
251
        load_options: &LoadOptions,
252
        aurl: &AllowedUrl,
253
    ) -> Result<SharedImageSurface, LoadingError> {
254
135
        match self.images.entry(aurl.clone()) {
255
110
            Entry::Occupied(e) => e.get().clone(),
256
46
            Entry::Vacant(e) => {
257
35
                let surface = load_image(load_options, e.key());
258
30
                let res = e.insert(surface);
259
61
                res.clone()
260
61
            }
261
31
        }
262
135
    }
263
}
264
2

            
265
32
fn load_image(
266
2
    load_options: &LoadOptions,
267
2
    aurl: &AllowedUrl,
268
2
) -> Result<SharedImageSurface, LoadingError> {
269
2
    let BinaryData {
270
32
        data: bytes,
271
30
        mime_type,
272
30
    } = io::acquire_data(aurl, None)?;
273

            
274
30
    if bytes.is_empty() {
275
        return Err(LoadingError::Other(String::from("no image data")));
276
    }
277

            
278
30
    let content_type = content_type_for_gdk_pixbuf(&mime_type);
279

            
280
57
    let loader = if let Some(ref content_type) = content_type {
281
27
        PixbufLoader::with_mime_type(content_type)?
282
2
    } else {
283
3
        PixbufLoader::new()
284
2
    };
285

            
286
30
    loader.write(&bytes)?;
287
30
    loader.close()?;
288

            
289
30
    let pixbuf = loader.pixbuf().ok_or_else(|| {
290
        LoadingError::Other(format!("loading image: {}", human_readable_url(aurl)))
291
    })?;
292

            
293
30
    let bytes = if load_options.keep_image_data {
294
        Some(bytes)
295
    } else {
296
30
        None
297
    };
298

            
299
30
    let surface = SharedImageSurface::from_pixbuf(&pixbuf, content_type.as_deref(), bytes)
300
30
        .map_err(|e| image_loading_error_from_cairo(e, aurl))?;
301

            
302
30
    Ok(surface)
303
30
}
304

            
305
32
fn content_type_for_gdk_pixbuf(mime_type: &Mime) -> Option<String> {
306
    // See issue #548 - data: URLs without a MIME-type automatically
307
    // fall back to "text/plain;charset=US-ASCII".  Some (old?) versions of
308
    // Adobe Illustrator generate data: URLs without MIME-type for image
309
    // data.  We'll catch this and fall back to sniffing by unsetting the
310
    // content_type.
311
32
    let unspecified_mime_type = Mime::from_str("text/plain;charset=US-ASCII").unwrap();
312
6

            
313
66
    if *mime_type == unspecified_mime_type {
314
10
        None
315
2
    } else {
316
32
        Some(format!("{}/{}", mime_type.type_, mime_type.subtype))
317
2
    }
318
34
}
319

            
320
2
fn human_readable_url(aurl: &AllowedUrl) -> &str {
321
    if aurl.scheme() == "data" {
322
2
        // avoid printing a huge data: URL for image data
323
2
        "data URL"
324
2
    } else {
325
        aurl.as_ref()
326
    }
327
2
}
328
4

            
329
4
fn image_loading_error_from_cairo(status: cairo::Error, aurl: &AllowedUrl) -> LoadingError {
330
2
    let url = human_readable_url(aurl);
331
4

            
332
8
    match status {
333
        cairo::Error::NoMemory => LoadingError::OutOfMemory(format!("loading image: {url}")),
334
        cairo::Error::InvalidSize => LoadingError::Other(format!("image too big: {url}")),
335
6
        _ => LoadingError::Other(format!("cairo error: {status}")),
336
    }
337
2
}
338

            
339
4
pub struct AcquiredNode {
340
    stack: Option<Rc<RefCell<NodeStack>>>,
341
2
    node: Node,
342
2
}
343
2

            
344
2
impl Drop for AcquiredNode {
345
2000871
    fn drop(&mut self) {
346
2000871
        if let Some(ref stack) = self.stack {
347
1500988
            let mut stack = stack.borrow_mut();
348
1500988
            let last = stack.pop().unwrap();
349
1500815
            assert!(last == self.node);
350
1500815
        }
351
2000633
    }
352
2
}
353

            
354
impl AcquiredNode {
355
1501056
    pub fn get(&self) -> &Node {
356
1501058
        &self.node
357
1501056
    }
358
2
}
359

            
360
/// Detects circular references between nodes, and enforces referencing limits.
361
///
362
2
/// Consider this fragment of SVG:
363
4
///
364
/// ```xml
365
/// <pattern id="foo">
366
///   <rect width="1" height="1" fill="url(#foo)"/>
367
2
/// </pattern>
368
/// ```
369
2
///
370
2
/// The pattern has a child element that references the pattern itself.  This kind of circular
371
2
/// reference is invalid.  The `AcquiredNodes` struct is passed around
372
/// wherever it may be necessary to resolve references to nodes, or to access nodes
373
/// "elsewhere" in the DOM that is not the current subtree.
374
2
///
375
2
/// Also, such constructs that reference other elements can be maliciously arranged like
376
2
/// in the [billion laughs attack][lol], to cause huge amounts of CPU to be consumed through
377
/// creating an exponential number of references.  `AcquiredNodes` imposes a hard limit on
378
2
/// the number of references that can be resolved for typical, well-behaved SVG documents.
379
///
380
2
/// The [`Self::acquire()`] and [`Self::acquire_ref()`] methods return an [`AcquiredNode`], which
381
/// acts like a smart pointer for a [`Node`].  Once a node has been acquired, it cannot be
382
/// acquired again until its [`AcquiredNode`] is dropped.  In the example above, a graphic element
383
/// would acquire the `pattern`, which would then acquire its `rect` child, which then would fail
384
/// to re-acquired the `pattern` — thus signaling a circular reference.
385
///
386
/// Those methods return an [`AcquireError`] to signal circular references.  Also, they
387
/// can return [`AcquireError::MaxReferencesExceeded`] if the aforementioned referencing limit
388
/// is exceeded.
389
///
390
/// [lol]: https://bitbucket.org/tiran/defusedxml
391
pub struct AcquiredNodes<'i> {
392
    document: &'i Document,
393
    num_elements_acquired: usize,
394
    node_stack: Rc<RefCell<NodeStack>>,
395
}
396

            
397
impl<'i> AcquiredNodes<'i> {
398
1056
    pub fn new(document: &Document) -> AcquiredNodes<'_> {
399
1056
        AcquiredNodes {
400
            document,
401
            num_elements_acquired: 0,
402
1056
            node_stack: Rc::new(RefCell::new(NodeStack::new())),
403
        }
404
1056
    }
405

            
406
136
    pub fn lookup_image(&self, href: &str) -> Result<SharedImageSurface, LoadingError> {
407
140
        self.document.lookup_image(href)
408
140
    }
409
4

            
410
    /// Acquires a node by its id.
411
4
    ///
412
4
    /// This is typically used during an "early resolution" stage, when XML `id`s are being
413
4
    /// resolved to node references.
414
1001075
    pub fn acquire(&mut self, node_id: &NodeId) -> Result<AcquiredNode, AcquireError> {
415
1001071
        self.num_elements_acquired += 1;
416

            
417
        // This is a mitigation for SVG files that try to instance a huge number of
418
        // elements via <use>, recursive patterns, etc.  See limits.rs for details.
419
1001071
        if self.num_elements_acquired > limits::MAX_REFERENCED_ELEMENTS {
420
13
            return Err(AcquireError::MaxReferencesExceeded);
421
        }
422
4

            
423
        // FIXME: callers shouldn't have to know that get_node() can initiate a file load.
424
        // Maybe we should have the following stages:
425
        //   - load main SVG XML
426
        //
427
        //   - load secondary SVG XML and other files like images; all document::Resources and
428
        //     document::Images loaded
429
        //
430
        //   - Now that all files are loaded, resolve URL references
431
2002116
        let node = self
432
            .document
433
            .lookup_node(node_id)
434
1001069
            .ok_or_else(|| AcquireError::LinkNotFound(node_id.clone()))?;
435

            
436
1001047
        if !node.is_element() {
437
            return Err(AcquireError::InvalidLinkType(node_id.clone()));
438
        }
439

            
440
1000842
        if node.borrow_element().is_accessed_by_reference() {
441
500819
            self.acquire_ref(&node)
442
        } else {
443
500191
            Ok(AcquiredNode { stack: None, node })
444
        }
445
1001035
    }
446

            
447
    /// Acquires a node whose reference is already known.
448
    ///
449
    /// This is useful for cases where a node is initially referenced by its id with
450
    /// [`Self::acquire`] and kept around for later use.  During the later use, the node
451
    /// needs to be re-acquired with this method.  For example:
452
    ///
453
    /// * At an "early resolution" stage, `acquire()` a pattern by its id, and keep around its
454
    /// [`Node`] reference.
455
    ///
456
    /// * At the drawing stage, `acquire_ref()` the pattern node that we already had, so that
457
31
    /// its child elements that reference other paint servers will be able to detect circular
458
62
    /// references to the pattern.
459
1501363
    pub fn acquire_ref(&self, node: &Node) -> Result<AcquiredNode, AcquireError> {
460
3002378
        if self.node_stack.borrow().contains(node) {
461
11
            Err(AcquireError::CircularReference(node.clone()))
462
        } else {
463
1501383
            self.node_stack.borrow_mut().push(node);
464
1500984
            Ok(AcquiredNode {
465
1501015
                stack: Some(self.node_stack.clone()),
466
1501015
                node: node.clone(),
467
31
            })
468
        }
469
1500995
    }
470
}
471

            
472
/// Keeps a stack of nodes and can check if a certain node is contained in the stack
473
///
474
/// Sometimes parts of the code cannot plainly use the implicit stack of acquired
475
/// nodes as maintained by DrawingCtx::acquire_node(), and they must keep their
476
/// own stack of nodes to test for reference cycles.  NodeStack can be used to do that.
477
31
pub struct NodeStack(Vec<Node>);
478

            
479
31
impl NodeStack {
480
501326
    pub fn new() -> NodeStack {
481
501326
        NodeStack(Vec::new())
482
501295
    }
483
31

            
484
1501220
    pub fn push(&mut self, node: &Node) {
485
1501251
        self.0.push(node.clone());
486
1501220
    }
487

            
488
1500820
    pub fn pop(&mut self) -> Option<Node> {
489
1500820
        self.0.pop()
490
1500820
    }
491
60

            
492
1501142
    pub fn contains(&self, node: &Node) -> bool {
493
16809150
        self.0.iter().any(|n| *n == *node)
494
1501137
    }
495
}
496

            
497
55
/// Used to build a tree of SVG nodes while an XML document is being read.
498
///
499
55
/// This struct holds the document-related state while loading an SVG document from XML:
500
/// the loading options, the partially-built tree of nodes, the CSS stylesheets that
501
55
/// appear while loading the document.
502
55
///
503
/// The XML loader asks a `DocumentBuilder` to
504
55
/// [`append_element`][DocumentBuilder::append_element],
505
55
/// [`append_characters`][DocumentBuilder::append_characters], etc.  When all the XML has
506
/// been consumed, the caller can use [`build`][DocumentBuilder::build] to get a
507
/// fully-loaded [`Document`].
508
pub struct DocumentBuilder {
509
    /// Metadata for the document's lifetime.
510
    session: Session,
511

            
512
    /// Loading options; mainly the URL resolver.
513
    load_options: Arc<LoadOptions>,
514

            
515
    /// Root node of the tree.
516
    tree: Option<Node>,
517

            
518
    /// Mapping from `id` attributes to nodes.
519
55
    ids: HashMap<String, Node>,
520
55

            
521
    /// Stylesheets defined in the document.
522
    stylesheets: Vec<Stylesheet>,
523
5
}
524

            
525
impl DocumentBuilder {
526
1081
    pub fn new(session: Session, load_options: Arc<LoadOptions>) -> DocumentBuilder {
527
1076
        DocumentBuilder {
528
1076
            session,
529
1136
            load_options,
530
1076
            tree: None,
531
1081
            ids: HashMap::new(),
532
1081
            stylesheets: Vec::new(),
533
        }
534
1091
    }
535

            
536
5
    /// Adds a stylesheet in order to the document.
537
11
    ///
538
12
    /// Stylesheets will later be matched in the order in which they were added.
539
47
    pub fn append_stylesheet(&mut self, stylesheet: Stylesheet) {
540
41
        self.stylesheets.push(stylesheet);
541
41
    }
542

            
543
    /// Creates an element of the specified `name` as a child of `parent`.
544
6
    ///
545
    /// This is the main function to create new SVG elements while parsing XML.
546
6
    ///
547
    /// `name` is the XML element's name, for example `rect`.
548
    ///
549
6
    /// `attrs` has the XML element's attributes, e.g. cx/cy/r for `<circle cx="0" cy="0"
550
    /// r="5">`.
551
    ///
552
5
    /// If `parent` is `None` it means that we are creating the root node in the tree of
553
5
    /// elements.  The code will later validate that this is indeed an `<svg>` element.
554
1022057
    pub fn append_element(
555
        &mut self,
556
        name: &QualName,
557
        attrs: Attributes,
558
        parent: Option<Node>,
559
    ) -> Node {
560
1022057
        let node = Node::new(NodeData::new_element(&self.session, name, attrs));
561

            
562
1021881
        if let Some(id) = node.borrow_element().get_id() {
563
            // This is so we don't overwrite an existing id
564
11905
            self.ids
565
11905
                .entry(id.to_string())
566
23798
                .or_insert_with(|| node.clone());
567
1022059
        }
568

            
569
1023129
        if let Some(parent) = parent {
570
1020997
            parent.append(node.clone());
571
1022045
        } else if self.tree.is_none() {
572
1066
            self.tree = Some(node.clone());
573
        } else {
574
            panic!("The tree root has already been set");
575
        }
576

            
577
1021859
        node
578
1021859
    }
579

            
580
    /// Creates a node for an XML text element as a child of `parent`.
581
1033794
    pub fn append_characters(&mut self, text: &str, parent: &mut Node) {
582
2067544
        if !text.is_empty() {
583
            // When the last child is a Chars node we can coalesce
584
            // the text and avoid screwing up the Pango layouts
585
2057044
            if let Some(child) = parent.last_child().filter(|c| c.is_chars()) {
586
2490
                child.borrow_chars().append(text);
587
2490
            } else {
588
1031304
                parent.append(Node::new(NodeData::new_chars(text)));
589
1033750
            };
590
        }
591
1033750
    }
592

            
593
    /// Does the final validation on the `Document` being read, and returns it.
594
1061
    pub fn build(self) -> Result<Document, LoadingError> {
595
        let DocumentBuilder {
596
1061
            load_options,
597
1061
            session,
598
1061
            tree,
599
1061
            ids,
600
1061
            stylesheets,
601
            ..
602
        } = self;
603

            
604
1061
        match tree {
605
1061
            Some(root) if root.is_element() => {
606
2119
                if is_element_of_type!(root, Svg) {
607
1058
                    let mut document = Document {
608
1058
                        tree: root,
609
1058
                        session: session.clone(),
610
1058
                        ids,
611
1058
                        externs: RefCell::new(Resources::new()),
612
1058
                        images: RefCell::new(Images::new()),
613
1058
                        load_options,
614
1058
                        stylesheets,
615
                    };
616

            
617
1058
                    document.cascade(&[], &session);
618

            
619
1058
                    Ok(document)
620
                } else {
621
3
                    Err(LoadingError::NoSvgRoot)
622
                }
623
1061
            }
624
            _ => Err(LoadingError::NoSvgRoot),
625
        }
626
1061
    }
627
}
628

            
629
#[cfg(test)]
630
mod tests {
631
    use super::*;
632

            
633
    #[test]
634
2
    fn parses_node_id() {
635
1
        assert_eq!(
636
1
            NodeId::parse("#foo").unwrap(),
637
1
            NodeId::Internal("foo".to_string())
638
        );
639

            
640
1
        assert_eq!(
641
1
            NodeId::parse("uri#foo").unwrap(),
642
1
            NodeId::External("uri".to_string(), "foo".to_string())
643
        );
644

            
645
1
        assert!(matches!(
646
1
            NodeId::parse("uri"),
647
            Err(NodeIdError::NodeIdRequired)
648
        ));
649
2
    }
650

            
651
    #[test]
652
2
    fn unspecified_mime_type_yields_no_content_type() {
653
        // Issue #548
654
1
        let mime = Mime::from_str("text/plain;charset=US-ASCII").unwrap();
655
1
        assert!(content_type_for_gdk_pixbuf(&mime).is_none());
656
2
    }
657

            
658
    #[test]
659
2
    fn strips_mime_type_parameters() {
660
        // Issue #699
661
1
        let mime = Mime::from_str("image/png;charset=utf-8").unwrap();
662
1
        assert_eq!(
663
1
            content_type_for_gdk_pixbuf(&mime),
664
1
            Some(String::from("image/png"))
665
        );
666
2
    }
667
}