[librsvg: 3/7] (#256): Fix matching of the systemLanguage attribute with the user's locale



commit ac5b5b1dbe0f80c1e4aee6983b9af1d5b3a0612b
Author: Federico Mena Quintero <federico gnome org>
Date:   Thu Oct 4 09:51:31 2018 -0500

    (#256): Fix matching of the systemLanguage attribute with the user's locale
    
    The SVG spec says that the systemLanguage attribute is a
    comma-separated list of BCP47 language tags, but librsvg was assuming
    that it was a list of X/Open locale names (e.g. what one would set in
    the LC_MESSAGES or LANG environment variables).
    
    Now, we convert the output of g_get_language_names() to BCP47 language
    tags using the locale_config crate.
    
    Then, we use the language_tags crate to actually match those tags
    against the values from the systemLanguage attribute.
    
    https://gitlab.gnome.org/GNOME/librsvg/issues/256

 rsvg_internals/src/cond.rs | 170 +++++++++++++++++++++++++++++++++------------
 rsvg_internals/src/lib.rs  |   2 +
 rsvg_internals/src/node.rs |  10 +--
 3 files changed, 135 insertions(+), 47 deletions(-)
---
diff --git a/rsvg_internals/src/cond.rs b/rsvg_internals/src/cond.rs
index b5fd9dad..63f77dce 100644
--- a/rsvg_internals/src/cond.rs
+++ b/rsvg_internals/src/cond.rs
@@ -1,9 +1,16 @@
-use error::*;
-use std::marker::PhantomData;
-
 #[allow(unused_imports, deprecated)]
 use std::ascii::AsciiExt;
 
+use std::str::FromStr;
+
+use glib;
+use itertools::{FoldWhile, Itertools};
+use language_tags::LanguageTag;
+use locale_config::{LanguageRange, Locale};
+
+use error::*;
+use parsers::ParseError;
+
 // No extensions at the moment.
 static IMPLEMENTED_EXTENSIONS: &[&str] = &[];
 
@@ -61,35 +68,103 @@ impl RequiredFeatures {
 }
 
 #[derive(Debug, PartialEq)]
-pub struct SystemLanguage<'a>(pub bool, pub PhantomData<&'a i8>);
-
-impl<'a> SystemLanguage<'a> {
-    // Parse a systemLanguage attribute
-    // http://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
-    pub fn from_attribute(
-        s: &str,
-        system_languages: &[String],
-    ) -> Result<SystemLanguage<'a>, ValueErrorKind> {
-        Ok(SystemLanguage(
-            s.split(',')
-                .map(|s| s.trim())
-                .filter(|s| !s.is_empty())
-                .any(|l| {
-                    system_languages.iter().any(|sl| {
-                        if sl.eq_ignore_ascii_case(l) {
-                            return true;
-                        }
+pub struct SystemLanguage(pub bool);
 
-                        if let Some(offset) = l.find('-') {
-                            return sl.eq_ignore_ascii_case(&l[..offset]);
+impl SystemLanguage {
+    /// Parse a `systemLanguage` attribute and match it against a given `Locale`
+    ///
+    /// The [`systemLanguage`] conditional attribute is a
+    /// comma-separated list of [BCP47] Language Tags.  This function
+    /// parses the attribute and matches the result against a given
+    /// `locale`.  If there is a match, i.e. if the given locale
+    /// supports one of the languages listed in the `systemLanguage`
+    /// attribute, then the `SystemLanguage.0` will be `true`;
+    /// otherwise it will be `false`.
+    ///
+    /// Normally, calling code will pass `&Locale::current()` for the
+    /// `locale` attribute; this is the user's current locale.
+    ///
+    /// [`systemLanguage`]: 
https://www.w3.org/TR/SVG/struct.html#ConditionalProcessingSystemLanguageAttribute
+    /// [BCP47]: http://www.ietf.org/rfc/bcp/bcp47.txt
+    pub fn from_attribute(s: &str, locale: &Locale) -> Result<SystemLanguage, ValueErrorKind> {
+        s.split(',')
+            .map(LanguageTag::from_str)
+            .fold_while(
+                // start with no match
+                Ok(SystemLanguage(false)),
+                // The accumulator is Result<SystemLanguage, ValueErrorKind>
+                |acc, tag_result| match tag_result {
+                    Ok(language_tag) => {
+                        let have_match = acc.unwrap().0;
+                        if have_match {
+                            FoldWhile::Continue(Ok(SystemLanguage(have_match)))
+                        } else {
+                            locale_accepts_language_tag(locale, &language_tag)
+                                .map(|matches| FoldWhile::Continue(Ok(SystemLanguage(matches))))
+                                .unwrap_or_else(|e| FoldWhile::Done(Err(e)))
                         }
+                    }
 
-                        false
-                    })
-                }),
-            PhantomData,
-        ))
+                    Err(e) => FoldWhile::Done(Err(ValueErrorKind::Parse(ParseError::new(
+                        &format!("invalid language tag: \"{}\"", e),
+                    )))),
+                },
+            )
+            .into_inner()
+    }
+}
+
+/// Gets the user's preferred locale from the environment and
+/// translates it to a `Locale` with `LanguageRange` fallbacks.
+///
+/// The `Locale::current()` call only contemplates a single language,
+/// but glib is smarter, and `g_get_langauge_names()` can provide
+/// fallbacks, for example, when LC_MESSAGES="en_US.UTF-8:de" (USA
+/// English and German).  This function converts the output of
+/// `g_get_language_names()` into a `Locale` with appropriate
+/// fallbacks.
+pub fn locale_from_environment() -> Result<Locale, String> {
+    let mut locale = Locale::invariant();
+
+    for name in glib::get_language_names() {
+        let range = LanguageRange::from_unix(&name).map_err(|e| format!("{}", e))?;
+        locale.add(&range);
+    }
+
+    Ok(locale)
+}
+
+fn locale_accepts_language_tag(
+    locale: &Locale,
+    language_tag: &LanguageTag,
+) -> Result<bool, ValueErrorKind> {
+    for locale_range in locale.tags_for("messages") {
+        if locale_range == LanguageRange::invariant() {
+            continue;
+        }
+
+        let str_locale_range = locale_range.as_ref();
+
+        let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| {
+            ValueErrorKind::Parse(ParseError::new(&format!(
+                "invalid language tag \"{}\" in locale: {}",
+                str_locale_range, e
+            )))
+        })?;
+
+        if !locale_tag.is_language_range() {
+            return Err(ValueErrorKind::Value(format!(
+                "language tag \"{}\" is not a language range",
+                locale_tag
+            )));
+        }
+
+        if locale_tag.matches(language_tag) {
+            return Ok(true);
+        }
     }
+
+    Ok(false)
 }
 
 #[cfg(test)]
@@ -135,41 +210,50 @@ mod tests {
 
     #[test]
     fn system_language() {
-        let system_languages = vec![String::from("de"), String::from("en_US")];
+        let user_prefers = Locale::new("de,en-US").unwrap();
+
+        assert!(SystemLanguage::from_attribute("", &user_prefers).is_err());
+
+        assert!(SystemLanguage::from_attribute("12345", &user_prefers).is_err());
+
+        assert_eq!(
+            SystemLanguage::from_attribute("fr", &user_prefers),
+            Ok(SystemLanguage(false))
+        );
 
         assert_eq!(
-            SystemLanguage::from_attribute("", &system_languages),
-            Ok(SystemLanguage(false, PhantomData))
+            SystemLanguage::from_attribute("en", &user_prefers),
+            Ok(SystemLanguage(false))
         );
 
         assert_eq!(
-            SystemLanguage::from_attribute("fr", &system_languages),
-            Ok(SystemLanguage(false, PhantomData))
+            SystemLanguage::from_attribute("de", &user_prefers),
+            Ok(SystemLanguage(true))
         );
 
         assert_eq!(
-            SystemLanguage::from_attribute("de", &system_languages),
-            Ok(SystemLanguage(true, PhantomData))
+            SystemLanguage::from_attribute("en-US", &user_prefers),
+            Ok(SystemLanguage(true))
         );
 
         assert_eq!(
-            SystemLanguage::from_attribute("en_US", &system_languages),
-            Ok(SystemLanguage(true, PhantomData))
+            SystemLanguage::from_attribute("en-GB", &user_prefers),
+            Ok(SystemLanguage(false))
         );
 
         assert_eq!(
-            SystemLanguage::from_attribute("DE", &system_languages),
-            Ok(SystemLanguage(true, PhantomData))
+            SystemLanguage::from_attribute("DE", &user_prefers),
+            Ok(SystemLanguage(true))
         );
 
         assert_eq!(
-            SystemLanguage::from_attribute("de-LU", &system_languages),
-            Ok(SystemLanguage(true, PhantomData))
+            SystemLanguage::from_attribute("de-LU", &user_prefers),
+            Ok(SystemLanguage(true))
         );
 
         assert_eq!(
-            SystemLanguage::from_attribute("fr, de", &system_languages),
-            Ok(SystemLanguage(true, PhantomData))
+            SystemLanguage::from_attribute("fr, de", &user_prefers),
+            Ok(SystemLanguage(true))
         );
     }
 }
diff --git a/rsvg_internals/src/lib.rs b/rsvg_internals/src/lib.rs
index 2dc9d95b..cb26e9f8 100644
--- a/rsvg_internals/src/lib.rs
+++ b/rsvg_internals/src/lib.rs
@@ -10,7 +10,9 @@ extern crate gdk_pixbuf;
 extern crate glib;
 extern crate glib_sys;
 extern crate itertools;
+extern crate language_tags;
 extern crate libc;
+extern crate locale_config;
 extern crate nalgebra;
 extern crate num_traits;
 extern crate owning_ref;
diff --git a/rsvg_internals/src/node.rs b/rsvg_internals/src/node.rs
index 0e394cad..0381dfa5 100644
--- a/rsvg_internals/src/node.rs
+++ b/rsvg_internals/src/node.rs
@@ -1,6 +1,5 @@
 use cairo::{Matrix, MatrixTrait};
 use downcast_rs::*;
-use glib;
 use glib::translate::*;
 use glib_sys;
 use std::cell::{Cell, Ref, RefCell};
@@ -8,7 +7,7 @@ use std::ptr;
 use std::rc::{Rc, Weak};
 
 use attributes::Attribute;
-use cond::{RequiredExtensions, RequiredFeatures, SystemLanguage};
+use cond::{locale_from_environment, RequiredExtensions, RequiredFeatures, SystemLanguage};
 use drawing_ctx::DrawingCtx;
 use error::*;
 use handle::{self, RsvgHandle};
@@ -390,8 +389,11 @@ impl Node {
                     }
 
                     Attribute::SystemLanguage if cond => {
-                        cond = SystemLanguage::from_attribute(value, &glib::get_language_names())
-                            .map(|SystemLanguage(res, _)| res)?;
+                        cond = SystemLanguage::from_attribute(
+                            value,
+                            &(locale_from_environment().map_err(|e| ValueErrorKind::Value(e))?),
+                        )
+                        .map(|SystemLanguage(res)| res)?;
                     }
 
                     _ => {}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]