1
//! 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

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

            
25
499
static UA_STYLESHEETS: Lazy<Vec<Stylesheet>> = Lazy::new(|| {
26
499
    vec![Stylesheet::from_data(
27
499
        include_str!("ua.css"),
28
499
        &UrlResolver::new(None),
29
499
        Origin::UserAgent,
30
499
    )
31
499
    .unwrap()]
32
499
});
33

            
34
/// Identifier of a node
35
3033149
#[derive(Debug, PartialEq, Clone)]
36
pub enum NodeId {
37
    /// element id
38
    Internal(String),
39
    /// url, element id
40
    External(String, String),
41
}
42

            
43
impl NodeId {
44
8112
    pub fn parse(href: &str) -> Result<NodeId, NodeIdError> {
45
8112
        let (url, id) = match href.rfind('#') {
46
272
            None => (Some(href), None),
47
7840
            Some(p) if p == 0 => (None, Some(&href[1..])),
48
84
            Some(p) => (Some(&href[..p]), Some(&href[(p + 1)..])),
49
        };
50

            
51
8112
        match (url, id) {
52
7756
            (None, Some(id)) if !id.is_empty() => Ok(NodeId::Internal(String::from(id))),
53
84
            (Some(url), Some(id)) if !id.is_empty() => {
54
84
                Ok(NodeId::External(String::from(url), String::from(id)))
55
            }
56
272
            _ => Err(NodeIdError::NodeIdRequired),
57
        }
58
8112
    }
59
}
60

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

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

            
75
    /// Mapping from `id` attributes to nodes.
76
    ids: HashMap<String, Node>,
77

            
78
    // The following two require interior mutability because we load the extern
79
    // resources all over the place.  Eventually we'll be able to do this
80
    // once, at loading time, and keep this immutable.
81
    /// SVG documents referenced from this document.
82
    externs: RefCell<Resources>,
83

            
84
    /// Image resources referenced from this document.
85
    images: RefCell<Images>,
86

            
87
    /// Used to load referenced resources.
88
    load_options: LoadOptions,
89

            
90
    /// Stylesheets defined in the document
91
    stylesheets: Vec<Stylesheet>,
92
}
93

            
94
impl Document {
95
    /// Constructs a `Document` by loading it from a stream.
96
6035
    pub fn load_from_stream(
97
6035
        load_options: &LoadOptions,
98
6035
        stream: &gio::InputStream,
99
6035
        cancellable: Option<&gio::Cancellable>,
100
6035
    ) -> Result<Document, LoadingError> {
101
6035
        xml_load_from_possibly_compressed_stream(
102
6035
            DocumentBuilder::new(load_options),
103
6035
            load_options.unlimited_size,
104
6035
            stream,
105
6035
            cancellable,
106
6035
        )
107
6035
    }
108

            
109
    /// Utility function to load a document from a static string in tests.
110
    #[cfg(test)]
111
5
    pub fn load_from_bytes(input: &'static [u8]) -> Document {
112
5
        use glib::prelude::*;
113
5

            
114
5
        let bytes = glib::Bytes::from_static(input);
115
5
        let stream = gio::MemoryInputStream::from_bytes(&bytes);
116
5

            
117
5
        Document::load_from_stream(
118
5
            &LoadOptions::new(UrlResolver::new(None)),
119
5
            &stream.upcast(),
120
5
            None::<&gio::Cancellable>,
121
5
        )
122
5
        .unwrap()
123
5
    }
124

            
125
    /// Gets the root node.  This is guaranteed to be an `<svg>` element.
126
23898
    pub fn root(&self) -> Node {
127
23898
        self.tree.clone()
128
23898
    }
129

            
130
    /// Looks up a node in this document or one of its resources by its `id` attribute.
131
6005370
    pub fn lookup_node(&self, node_id: &NodeId) -> Option<Node> {
132
6005370
        match node_id {
133
6005304
            NodeId::Internal(id) => self.lookup_internal_node(id),
134
66
            NodeId::External(url, id) => self
135
66
                .externs
136
66
                .borrow_mut()
137
66
                .lookup(&self.load_options, url, id)
138
66
                .ok(),
139
        }
140
6005370
    }
141

            
142
    /// Looks up a node in this document by its `id` attribute.
143
6005957
    pub fn lookup_internal_node(&self, id: &str) -> Option<Node> {
144
6005957
        self.ids.get(id).map(|n| (*n).clone())
145
6005957
    }
146

            
147
    /// Loads an image by URL, or returns a pre-loaded one.
148
816
    pub fn lookup_image(&self, url: &str) -> Result<SharedImageSurface, LoadingError> {
149
816
        let aurl = self
150
816
            .load_options
151
816
            .url_resolver
152
816
            .resolve_href(url)
153
816
            .map_err(|_| LoadingError::BadUrl)?;
154

            
155
810
        self.images.borrow_mut().lookup(&self.load_options, &aurl)
156
816
    }
157

            
158
    /// Runs the CSS cascade on the document tree
159
    ///
160
    /// This uses the default UserAgent stylesheet, the document's internal stylesheets,
161
    /// plus an extra set of stylesheets supplied by the caller.
162
5963
    pub fn cascade(&mut self, extra: &[Stylesheet]) {
163
5963
        css::cascade(&mut self.tree, &UA_STYLESHEETS, &self.stylesheets, extra);
164
5963
    }
165
}
166

            
167
struct Resources {
168
    resources: HashMap<AllowedUrl, Result<Rc<Document>, LoadingError>>,
169
}
170

            
171
impl Resources {
172
5945
    pub fn new() -> Resources {
173
5945
        Resources {
174
5945
            resources: Default::default(),
175
5945
        }
176
5945
    }
177

            
178
66
    pub fn lookup(
179
66
        &mut self,
180
66
        load_options: &LoadOptions,
181
66
        url: &str,
182
66
        id: &str,
183
66
    ) -> Result<Node, LoadingError> {
184
66
        self.get_extern_document(load_options, url)
185
66
            .and_then(|doc| doc.lookup_internal_node(id).ok_or(LoadingError::BadUrl))
186
66
    }
187

            
188
66
    fn get_extern_document(
189
66
        &mut self,
190
66
        load_options: &LoadOptions,
191
66
        href: &str,
192
66
    ) -> Result<Rc<Document>, LoadingError> {
193
66
        let aurl = load_options
194
66
            .url_resolver
195
66
            .resolve_href(href)
196
66
            .map_err(|_| LoadingError::BadUrl)?;
197

            
198
42
        match self.resources.entry(aurl) {
199
24
            Entry::Occupied(e) => e.get().clone(),
200
18
            Entry::Vacant(e) => {
201
18
                let aurl = e.key();
202
18
                // FIXME: pass a cancellable to these
203
18
                let doc = io::acquire_stream(aurl, None)
204
18
                    .map_err(LoadingError::from)
205
18
                    .and_then(|stream| {
206
18
                        Document::load_from_stream(
207
18
                            &load_options.copy_with_base_url(aurl),
208
18
                            &stream,
209
18
                            None,
210
18
                        )
211
18
                    })
212
18
                    .map(Rc::new);
213
18
                let res = e.insert(doc);
214
18
                res.clone()
215
            }
216
        }
217
66
    }
218
}
219

            
220
struct Images {
221
    images: HashMap<AllowedUrl, Result<SharedImageSurface, LoadingError>>,
222
}
223

            
224
impl Images {
225
5945
    fn new() -> Images {
226
5945
        Images {
227
5945
            images: Default::default(),
228
5945
        }
229
5945
    }
230

            
231
810
    fn lookup(
232
810
        &mut self,
233
810
        load_options: &LoadOptions,
234
810
        aurl: &AllowedUrl,
235
810
    ) -> Result<SharedImageSurface, LoadingError> {
236
810
        match self.images.entry(aurl.clone()) {
237
630
            Entry::Occupied(e) => e.get().clone(),
238
180
            Entry::Vacant(e) => {
239
180
                let surface = load_image(load_options, e.key());
240
180
                let res = e.insert(surface);
241
180
                res.clone()
242
            }
243
        }
244
810
    }
245
}
246

            
247
180
fn load_image(
248
180
    load_options: &LoadOptions,
249
180
    aurl: &AllowedUrl,
250
180
) -> Result<SharedImageSurface, LoadingError> {
251
    let BinaryData {
252
180
        data: bytes,
253
180
        mime_type,
254
180
    } = io::acquire_data(aurl, None)?;
255

            
256
180
    if bytes.is_empty() {
257
        return Err(LoadingError::Other(String::from("no image data")));
258
180
    }
259
180

            
260
180
    let content_type = content_type_for_gdk_pixbuf(&mime_type);
261

            
262
180
    let loader = if let Some(ref content_type) = content_type {
263
162
        PixbufLoader::with_mime_type(content_type)?
264
    } else {
265
18
        PixbufLoader::new()
266
    };
267

            
268
180
    loader.write(&bytes)?;
269
180
    loader.close()?;
270

            
271
180
    let pixbuf = loader.pixbuf().ok_or_else(|| {
272
        LoadingError::Other(format!("loading image: {}", human_readable_url(aurl)))
273
180
    })?;
274

            
275
180
    let bytes = if load_options.keep_image_data {
276
        Some(bytes)
277
    } else {
278
180
        None
279
    };
280

            
281
180
    let surface = SharedImageSurface::from_pixbuf(&pixbuf, content_type.as_deref(), bytes)
282
180
        .map_err(|e| image_loading_error_from_cairo(e, aurl))?;
283

            
284
180
    Ok(surface)
285
180
}
286

            
287
182
fn content_type_for_gdk_pixbuf(mime_type: &Mime) -> Option<String> {
288
182
    // See issue #548 - data: URLs without a MIME-type automatically
289
182
    // fall back to "text/plain;charset=US-ASCII".  Some (old?) versions of
290
182
    // Adobe Illustrator generate data: URLs without MIME-type for image
291
182
    // data.  We'll catch this and fall back to sniffing by unsetting the
292
182
    // content_type.
293
182
    let unspecified_mime_type = Mime::from_str("text/plain;charset=US-ASCII").unwrap();
294
182

            
295
182
    if *mime_type == unspecified_mime_type {
296
19
        None
297
    } else {
298
163
        Some(format!("{}/{}", mime_type.type_, mime_type.subtype))
299
    }
300
182
}
301

            
302
fn human_readable_url(aurl: &AllowedUrl) -> &str {
303
    if aurl.scheme() == "data" {
304
        // avoid printing a huge data: URL for image data
305
        "data URL"
306
    } else {
307
        aurl.as_ref()
308
    }
309
}
310

            
311
fn image_loading_error_from_cairo(status: cairo::Error, aurl: &AllowedUrl) -> LoadingError {
312
    let url = human_readable_url(aurl);
313

            
314
    match status {
315
        cairo::Error::NoMemory => LoadingError::OutOfMemory(format!("loading image: {}", url)),
316
        cairo::Error::InvalidSize => LoadingError::Other(format!("image too big: {}", url)),
317
        _ => LoadingError::Other(format!("cairo error: {}", status)),
318
    }
319
}
320

            
321
pub struct AcquiredNode {
322
    stack: Option<Rc<RefCell<NodeStack>>>,
323
    node: Node,
324
}
325

            
326
impl Drop for AcquiredNode {
327
12006540
    fn drop(&mut self) {
328
12006540
        if let Some(ref stack) = self.stack {
329
9005406
            let mut stack = stack.borrow_mut();
330
9005406
            let last = stack.pop().unwrap();
331
9005406
            assert!(last == self.node);
332
3001134
        }
333
12007704
    }
334
}
335

            
336
impl AcquiredNode {
337
9006816
    pub fn get(&self) -> &Node {
338
9006816
        &self.node
339
9006816
    }
340
}
341

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

            
379
impl<'i> AcquiredNodes<'i> {
380
6062
    pub fn new(document: &Document) -> AcquiredNodes<'_> {
381
6062
        AcquiredNodes {
382
6062
            document,
383
6062
            num_elements_acquired: 0,
384
6062
            node_stack: Rc::new(RefCell::new(NodeStack::new())),
385
6062
        }
386
6062
    }
387

            
388
816
    pub fn lookup_image(&self, href: &str) -> Result<SharedImageSurface, LoadingError> {
389
816
        self.document.lookup_image(href)
390
816
    }
391

            
392
    /// Acquires a node by its id.
393
    ///
394
    /// This is typically used during an "early resolution" stage, when XML `id`s are being
395
    /// resolved to node references.
396
6005814
    pub fn acquire(&mut self, node_id: &NodeId) -> Result<AcquiredNode, AcquireError> {
397
6005814
        self.num_elements_acquired += 1;
398
6005814

            
399
6005814
        // This is a mitigation for SVG files that try to instance a huge number of
400
6005814
        // elements via <use>, recursive patterns, etc.  See limits.rs for details.
401
6005814
        if self.num_elements_acquired > limits::MAX_REFERENCED_ELEMENTS {
402
78
            return Err(AcquireError::MaxReferencesExceeded);
403
6005736
        }
404

            
405
        // FIXME: callers shouldn't have to know that get_node() can initiate a file load.
406
        // Maybe we should have the following stages:
407
        //   - load main SVG XML
408
        //
409
        //   - load secondary SVG XML and other files like images; all document::Resources and
410
        //     document::Images loaded
411
        //
412
        //   - Now that all files are loaded, resolve URL references
413
6005736
        let node = self
414
6005736
            .document
415
6005736
            .lookup_node(node_id)
416
6005736
            .ok_or_else(|| AcquireError::LinkNotFound(node_id.clone()))?;
417

            
418
6005670
        if !node.is_element() {
419
            return Err(AcquireError::InvalidLinkType(node_id.clone()));
420
6005670
        }
421
6005670

            
422
6005670
        if node.borrow_element().is_accessed_by_reference() {
423
3004626
            self.acquire_ref(&node)
424
        } else {
425
3001134
            Ok(AcquiredNode { stack: None, node })
426
        }
427
6005904
    }
428

            
429
    /// Acquires a node whose reference is already known.
430
    ///
431
    /// This is useful for cases where a node is initially referenced by its id with
432
    /// [`Self::acquire`] and kept around for later use.  During the later use, the node
433
    /// needs to be re-acquired with this method.  For example:
434
    ///
435
    /// * At an "early resolution" stage, `acquire()` a pattern by its id, and keep around its
436
    /// [`Node`] reference.
437
    ///
438
    /// * At the drawing stage, `acquire_ref()` the pattern node that we already had, so that
439
    /// its child elements that reference other paint servers will be able to detect circular
440
    /// references to the pattern.
441
9006696
    pub fn acquire_ref(&self, node: &Node) -> Result<AcquiredNode, AcquireError> {
442
9006696
        if self.node_stack.borrow().contains(node) {
443
66
            Err(AcquireError::CircularReference(node.clone()))
444
        } else {
445
9006630
            self.node_stack.borrow_mut().push(node);
446
9006630
            Ok(AcquiredNode {
447
9006630
                stack: Some(self.node_stack.clone()),
448
9006630
                node: node.clone(),
449
9006630
            })
450
        }
451
9006696
    }
452
}
453

            
454
/// Keeps a stack of nodes and can check if a certain node is contained in the stack
455
///
456
/// Sometimes parts of the code cannot plainly use the implicit stack of acquired
457
/// nodes as maintained by DrawingCtx::acquire_node(), and they must keep their
458
/// own stack of nodes to test for reference cycles.  NodeStack can be used to do that.
459
pub struct NodeStack(Vec<Node>);
460

            
461
impl NodeStack {
462
3007352
    pub fn new() -> NodeStack {
463
3007352
        NodeStack(Vec::new())
464
3007352
    }
465

            
466
9006786
    pub fn push(&mut self, node: &Node) {
467
9006786
        self.0.push(node.clone());
468
9006786
    }
469

            
470
9006600
    pub fn pop(&mut self) -> Option<Node> {
471
9006600
        self.0.pop()
472
9006600
    }
473

            
474
9006678
    pub fn contains(&self, node: &Node) -> bool {
475
91883700
        self.0.iter().any(|n| *n == *node)
476
9006678
    }
477
}
478

            
479
pub struct DocumentBuilder {
480
    load_options: LoadOptions,
481
    tree: Option<Node>,
482
    ids: HashMap<String, Node>,
483
    stylesheets: Vec<Stylesheet>,
484
}
485

            
486
impl DocumentBuilder {
487
6035
    pub fn new(load_options: &LoadOptions) -> DocumentBuilder {
488
6035
        DocumentBuilder {
489
6035
            load_options: load_options.clone(),
490
6035
            tree: None,
491
6035
            ids: HashMap::new(),
492
6035
            stylesheets: Vec::new(),
493
6035
        }
494
6035
    }
495

            
496
12
    pub fn append_stylesheet_from_xml_processing_instruction(
497
12
        &mut self,
498
12
        alternate: Option<String>,
499
12
        type_: Option<String>,
500
12
        href: &str,
501
12
    ) -> Result<(), LoadingError> {
502
12
        if type_.as_deref() != Some("text/css")
503
12
            || (alternate.is_some() && alternate.as_deref() != Some("no"))
504
        {
505
            return Err(LoadingError::Other(String::from(
506
                "invalid parameters in XML processing instruction for stylesheet",
507
            )));
508
12
        }
509

            
510
        // FIXME: handle CSS errors
511
6
        if let Ok(stylesheet) =
512
12
            Stylesheet::from_href(href, &self.load_options.url_resolver, Origin::Author)
513
6
        {
514
6
            self.stylesheets.push(stylesheet);
515
6
        }
516

            
517
12
        Ok(())
518
12
    }
519

            
520
6128660
    pub fn append_element(
521
6128660
        &mut self,
522
6128660
        name: &QualName,
523
6128660
        attrs: Attributes,
524
6128660
        parent: Option<Node>,
525
6128660
    ) -> Node {
526
6128660
        let node = Node::new(NodeData::new_element(name, attrs));
527

            
528
6128660
        if let Some(id) = node.borrow_element().get_id() {
529
69722
            // This is so we don't overwrite an existing id
530
69722
            self.ids
531
69722
                .entry(id.to_string())
532
69722
                .or_insert_with(|| node.clone());
533
6058940
        }
534

            
535
6128660
        if let Some(mut parent) = parent {
536
6122673
            parent.append(node.clone());
537
6122673
        } else if self.tree.is_none() {
538
5987
            self.tree = Some(node.clone());
539
5987
        } else {
540
            panic!("The tree root has already been set");
541
        }
542

            
543
6128660
        node
544
6128660
    }
545

            
546
    pub fn append_stylesheet_from_text(&mut self, text: &str) {
547
        // FIXME: handle CSS errors
548
240
        if let Ok(stylesheet) =
549
240
            Stylesheet::from_data(text, &self.load_options.url_resolver, Origin::Author)
550
240
        {
551
240
            self.stylesheets.push(stylesheet);
552
240
        }
553
240
    }
554

            
555
6198276
    pub fn append_characters(&mut self, text: &str, parent: &mut Node) {
556
6198295
        if !text.is_empty() {
557
6198295
            self.append_chars_to_parent(text, parent);
558
        }
559
6198276
    }
560

            
561
6198295
    fn append_chars_to_parent(&mut self, text: &str, parent: &mut Node) {
562
        // When the last child is a Chars node we can coalesce
563
        // the text and avoid screwing up the Pango layouts
564
6198295
        if let Some(child) = parent.last_child().filter(|c| c.is_chars()) {
565
14842
            child.borrow_chars().append(text);
566
6183453
        } else {
567
6183453
            parent.append(Node::new(NodeData::new_chars(text)));
568
6183453
        };
569
6198295
    }
570

            
571
30
    pub fn resolve_href(&self, href: &str) -> Result<AllowedUrl, AllowedUrlError> {
572
30
        self.load_options.url_resolver.resolve_href(href)
573
30
    }
574

            
575
5963
    pub fn build(self) -> Result<Document, LoadingError> {
576
5963
        let DocumentBuilder {
577
5963
            load_options,
578
5963
            tree,
579
5963
            ids,
580
5963
            stylesheets,
581
            ..
582
        } = self;
583

            
584
5963
        match tree {
585
5963
            Some(root) if root.is_element() => {
586
5963
                if is_element_of_type!(root, Svg) {
587
5945
                    let mut document = Document {
588
5945
                        tree: root,
589
5945
                        ids,
590
5945
                        externs: RefCell::new(Resources::new()),
591
5945
                        images: RefCell::new(Images::new()),
592
5945
                        load_options,
593
5945
                        stylesheets,
594
5945
                    };
595
5945

            
596
5945
                    document.cascade(&[]);
597
5945

            
598
5945
                    Ok(document)
599
                } else {
600
18
                    Err(LoadingError::NoSvgRoot)
601
                }
602
            }
603
            _ => Err(LoadingError::NoSvgRoot),
604
        }
605
5963
    }
606
}
607

            
608
#[cfg(test)]
609
mod tests {
610
    use super::*;
611

            
612
1
    #[test]
613
1
    fn parses_node_id() {
614
1
        assert_eq!(
615
1
            NodeId::parse("#foo").unwrap(),
616
1
            NodeId::Internal("foo".to_string())
617
1
        );
618

            
619
1
        assert_eq!(
620
1
            NodeId::parse("uri#foo").unwrap(),
621
1
            NodeId::External("uri".to_string(), "foo".to_string())
622
1
        );
623

            
624
1
        assert!(matches!(
625
1
            NodeId::parse("uri"),
626
            Err(NodeIdError::NodeIdRequired)
627
        ));
628
1
    }
629

            
630
1
    #[test]
631
1
    fn unspecified_mime_type_yields_no_content_type() {
632
1
        // Issue #548
633
1
        let mime = Mime::from_str("text/plain;charset=US-ASCII").unwrap();
634
1
        assert!(content_type_for_gdk_pixbuf(&mime).is_none());
635
1
    }
636

            
637
1
    #[test]
638
1
    fn strips_mime_type_parameters() {
639
1
        // Issue #699
640
1
        let mime = Mime::from_str("image/png;charset=utf-8").unwrap();
641
1
        assert_eq!(
642
1
            content_type_for_gdk_pixbuf(&mime),
643
1
            Some(String::from("image/png"))
644
1
        );
645
1
    }
646
}