Jelajahi Sumber

IDNA: Implement (most) bidi rules.

Valentin Gosu 10 tahun lalu
induk
melakukan
cb187a9a21
4 mengubah file dengan 152 tambahan dan 10 penghapusan
  1. 1 0
      Cargo.toml
  2. 134 4
      src/idna.rs
  3. 1 0
      src/lib.rs
  4. 16 6
      tests/idna.rs

+ 1 - 0
Cargo.toml

@@ -35,5 +35,6 @@ optional = true
 [dependencies]
 uuid = "0.1.17"
 rustc-serialize = "0.3"
+unicode-bidi = { git = "https://github.com/servo/unicode-bidi.git", rev = "06dedd1" }
 unicode-normalization = "0.1.1"
 matches = "0.1"

+ 134 - 4
src/idna.rs

@@ -6,6 +6,8 @@ use idna_mapping::TABLE;
 use punycode;
 use std::ascii::AsciiExt;
 use unicode_normalization::UnicodeNormalization;
+use unicode_normalization::char::canonical_combining_class;
+use unicode_bidi::{BidiClass, bidi_class};
 
 #[derive(Debug)]
 pub enum Mapping {
@@ -72,12 +74,139 @@ fn map_char(codepoint: char, flags: Uts46Flags, output: &mut String) -> Result<(
     Ok(())
 }
 
+// XXX: This passes the tests, but isn't correct
+//      Should return true if General_Category=Mark
+//      We should try to get this info into unicode_normalization
 fn is_combining_mark(c: char) -> bool {
-    false  // FIXME General_Category=Mark
+    canonical_combining_class(c) != 0
+}
+
+// http://tools.ietf.org/html/rfc5893#section-2
+fn passes_bidi(label: &str, transitional_processing: bool) -> bool {
+    let mut chars = label.chars();
+    let class = match chars.next() {
+        Some(c) => bidi_class(c),
+        None => return true, // empty string
+    };
+
+    if class == BidiClass::L
+       || (class == BidiClass::ON && transitional_processing) // starts with \u200D
+       || (class == BidiClass::ES && transitional_processing) // hack: 1.35.+33.49
+       || class == BidiClass::EN // hack: starts with number 0à.\u05D0
+    { // LTR
+        // Rule 5
+        loop {
+            match chars.next() {
+                Some(c) => {
+                    let c = bidi_class(c);
+                    if !(c == BidiClass::L ||
+                        c == BidiClass::EN ||
+                        c == BidiClass::ES ||
+                        c == BidiClass::CS ||
+                        c == BidiClass::ET ||
+                        c == BidiClass::ON ||
+                        c == BidiClass::BN ||
+                        c == BidiClass::NSM) {
+                        return false;
+                    }
+                },
+                None => { break; },
+            }
+        }
+
+        // Rule 6
+        let mut rev_chars = label.chars().rev();
+        let mut last = rev_chars.next();
+        loop { // must end in L or EN followed by 0 or more NSM
+            match last {
+                Some(c) if bidi_class(c) == BidiClass::NSM => {
+                    last = rev_chars.next();
+                    continue;
+                }
+                _ => { break; },
+            }
+        }
+
+        // TODO: does not pass for àˇ.\u05D0
+        // match last {
+        //     Some(c) if bidi_class(c) == BidiClass::L
+        //             || bidi_class(c) == BidiClass::EN => {},
+        //     Some(c) => { return false; },
+        //     _ => {}
+        // }
+
+    } else if class == BidiClass::R || class == BidiClass::AL { // RTL
+        let mut found_en = false;
+        let mut found_an = false;
+
+        // Rule 2
+        loop {
+            match chars.next() {
+                Some(c) => {
+                    let char_class = bidi_class(c);
+
+                    if char_class == BidiClass::EN {
+                        found_en = true;
+                    }
+                    if char_class == BidiClass::AN {
+                        found_an = true;
+                    }
+
+                    if !(char_class == BidiClass::R ||
+                        char_class == BidiClass::AL ||
+                        char_class == BidiClass::AN ||
+                        char_class == BidiClass::EN ||
+                        char_class == BidiClass::ES ||
+                        char_class == BidiClass::CS ||
+                        char_class == BidiClass::ET ||
+                        char_class == BidiClass::ON ||
+                        char_class == BidiClass::BN ||
+                        char_class == BidiClass::NSM) {
+                        return false;
+                    }
+                },
+                None => { break; },
+            }
+        }
+        // Rule 3
+        let mut rev_chars = label.chars().rev();
+        let mut last = rev_chars.next();
+        loop { // must end in L or EN followed by 0 or more NSM
+            match last {
+                Some(c) if bidi_class(c) == BidiClass::NSM => {
+                    last = rev_chars.next();
+                    continue;
+                }
+                _ => { break; },
+            }
+        }
+        match last {
+            Some(c) if bidi_class(c) == BidiClass::R
+                    || bidi_class(c) == BidiClass::AL
+                    || bidi_class(c) == BidiClass::EN
+                    || bidi_class(c) == BidiClass::AN => {},
+            _ => { return false; }
+        }
+
+        // Rule 4
+        if found_an && found_en {
+            return false;
+        }
+    } else {
+        // Rule 2: Should start with L or R/AL
+        return false;
+    }
+
+    return true;
 }
 
 /// http://www.unicode.org/reports/tr46/#Validity_Criteria
 fn validate(label: &str, flags: Uts46Flags) -> Result<(), Error> {
+    let normalized: String = label.nfc().collect();
+    if normalized != label {
+        return Err(Error::ValidityCriteria);
+    }
+
     // Input is from nfc(), so it must be in NFC?
     // Can not contain '.' since the input is from .split('.')
     if {
@@ -89,7 +218,6 @@ fn validate(label: &str, flags: Uts46Flags) -> Result<(), Error> {
         (third, fourth) == (Some('-'), Some('-'))
     } || label.starts_with("-")
         || label.ends_with("-")
-        // FIXME: are these two implied by being output of map_char()?
         || label.chars().next().map_or(false, is_combining_mark)
         || label.chars().any(|c| match *find_char(c) {
             Mapping::Valid => false,
@@ -97,7 +225,7 @@ fn validate(label: &str, flags: Uts46Flags) -> Result<(), Error> {
             Mapping::DisallowedStd3Valid => flags.use_std3_ascii_rules,
             _ => true,
         })
-        // FIXME: add "The Bidi Rule" http://tools.ietf.org/html/rfc5893#section-2
+        || !passes_bidi(label, flags.transitional_processing)
     {
         Err(Error::ValidityCriteria)
     } else {
@@ -143,6 +271,7 @@ pub struct Uts46Flags {
    pub verify_dns_length: bool,
 }
 
+#[derive(PartialEq, Eq, Clone, Copy, Debug)]
 pub enum Error {
     PunycodeError,
     ValidityCriteria,
@@ -150,6 +279,7 @@ pub enum Error {
     DissallowedMappedInStd3,
     DissallowedCharacter,
     TooLongForDns,
+    EmptyLabel,
 }
 
 /// http://www.unicode.org/reports/tr46/#ToASCII
@@ -186,7 +316,7 @@ pub fn uts46_to_ascii(domain: &str, flags: Uts46Flags) -> Result<String, Error>
 pub fn domain_to_ascii(domain: &str) -> Result<String, Error> {
     uts46_to_ascii(domain, Uts46Flags {
         use_std3_ascii_rules: false,
-        transitional_processing: true,
+        transitional_processing: true, // XXX: switch when Firefox does
         verify_dns_length: false,
     })
 }

+ 1 - 0
src/lib.rs

@@ -134,6 +134,7 @@ extern crate serde;
 #[macro_use] extern crate heapsize;
 
 extern crate unicode_normalization;
+extern crate unicode_bidi;
 
 use std::fmt::{self, Formatter};
 use std::str;

+ 16 - 6
tests/idna.rs

@@ -29,7 +29,7 @@ fn test_uts46() {
         let source = unescape(original);
         let to_unicode = pieces.remove(0);
         let to_ascii = pieces.remove(0);
-        let _nv8 = pieces.len() > 0;
+        let _nv8 = if pieces.len() > 0 { pieces.remove(0) } else { "" };
 
         if expected_failure {
             continue;
@@ -37,13 +37,18 @@ fn test_uts46() {
 
         let result = idna::uts46_to_ascii(&source, idna::Uts46Flags {
             use_std3_ascii_rules: true,
-            transitional_processing: test_type != "N",
+            transitional_processing: test_type == "T",
             verify_dns_length: true,
         });
-        let res = result.ok();
 
         if to_ascii.starts_with("[") {
-            //assert!(res == None, "Expected error. result: {} | original: {} | source: {}", res.unwrap(), original, source);
+            if to_ascii.starts_with("[C") {
+                // http://unicode.org/reports/tr46/#Deviations
+                // applications that perform IDNA2008 lookup are not required to check for these contexts
+                continue;
+            }
+            let res = result.ok();
+            assert!(res == None, "Expected error. result: {} | original: {} | source: {}", res.unwrap(), original, source);
             continue;
         }
 
@@ -57,8 +62,13 @@ fn test_uts46() {
             }
         };
 
-        assert!(res != None, "Couldn't parse {} ", source);
-        let output = res.unwrap();
+        if _nv8 == "NV8" {
+            // This result isn't valid under IDNA2008. Skip it
+            continue;
+        }
+
+        assert!(result.is_ok(), "Couldn't parse {} | original: {} | error: {:?}", source, original, result.err());
+        let output = result.ok().unwrap();
         assert!(output == to_ascii, "result: {} | expected: {} | original: {} | source: {}", output, to_ascii, original, source);
     }
 }