OSDN Git Service

投稿欄にIME経由で絵文字を入力するとエラーが発生する問題を回避
authorKimura Youichi <kim.upsilon@bucyou.net>
Sat, 11 Nov 2017 20:04:15 +0000 (05:04 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Sat, 11 Nov 2017 20:04:15 +0000 (05:04 +0900)
TextBox に IME 経由 (ATOK2017で確認) で絵文字を入力した場合に、サロゲートペアのうち
LowSurrogate が入力される前に TextChanged イベントが発生することによって
壊れたエンコーディングの文字列が Twitter.GetTextLengthRemain に渡される場合があった

Fixes: 33ea8c0b ("ツイート文字数の280文字への上限緩和 (weightedLength) に対応")

OpenTween.Tests/ExtensionsTest.cs
OpenTween.Tests/TwitterTest.cs
OpenTween/Extensions.cs
OpenTween/Twitter.cs

index 023d948..922306e 100644 (file)
@@ -55,5 +55,34 @@ namespace OpenTween
             Assert.True(CultureInfo.InvariantCulture.Contains(new CultureInfo("ja")));
             Assert.True(CultureInfo.InvariantCulture.Contains(CultureInfo.InvariantCulture));
         }
+
+        [Theory]
+        [InlineData("abc", 0, (int)'a')]
+        [InlineData("abc", 1, (int)'b')]
+        [InlineData("abc", 2, (int)'c')]
+        [InlineData("🍣", 0, 0x1f363)] // サロゲートペア
+        public void GetCodepointAtSafe_Test(string s, int index, int expected)
+        {
+            Assert.Equal(expected, s.GetCodepointAtSafe(index));
+        }
+
+        [Theory]
+        // char.ConvertToUtf32 をそのまま使用するとエラーになるパターン
+        [InlineData(new[] { '\ud83c', '\udf63' }, 1, 0xdf63)] // pos がサロゲートペアの後半部分を指している
+        [InlineData(new[] { '\ud83c' }, 0, 0xd83c)] // 壊れたサロゲートペア (LowSurrogate が無い)
+        [InlineData(new[] { '\udf63' }, 0, 0xdf63)] // 壊れたサロゲートペア (HighSurrogate が無い)
+        public void GetCodepointAtSafe_BrokenSurrogateTest(char[] s, int index, int expected)
+        {
+            // InlineDataAttribute で壊れたサロゲートペアの string を扱えないため char[] を使う
+            Assert.Equal(expected, new string(s).GetCodepointAtSafe(index));
+        }
+
+        [Fact]
+        public void GetCodepointAtSafe_ErrorTest()
+        {
+            Assert.Throws<ArgumentNullException>(() => ((string)null).GetCodepointAtSafe(0));
+            Assert.Throws<ArgumentOutOfRangeException>(() => "a".GetCodepointAtSafe(-1));
+            Assert.Throws<ArgumentOutOfRangeException>(() => "a".GetCodepointAtSafe(1));
+        }
     }
 }
index f75fa41..d61729a 100644 (file)
@@ -397,5 +397,16 @@ namespace OpenTween
                 Assert.Equal(267, twitter.GetTextLengthRemain("🔥🐔🔥 焼き鳥"));
             }
         }
+
+        [Fact]
+        public void GetTextLengthRemain_BrokenSurrogateTest()
+        {
+            using (var twitter = new Twitter())
+            {
+                // 投稿欄に IME から絵文字を入力すると HighSurrogate のみ入力された状態で TextChanged イベントが呼ばれることがある
+                Assert.Equal(278, twitter.GetTextLengthRemain("\ud83d"));
+                Assert.Equal(9999, twitter.GetTextLengthRemain("D twitter \ud83d"));
+            }
+        }
     }
 }
index b712c83..313884b 100644 (file)
@@ -73,5 +73,17 @@ namespace OpenTween
             key = kvp.Key;
             value = kvp.Value;
         }
+
+        /// <summary>
+        /// 文字列中の指定された位置にある文字のコードポイントを返します
+        /// </summary>
+        public static int GetCodepointAtSafe(this string s, int index)
+        {
+            // IsSurrogatePair が true を返す場合のみ ConvertToUtf32 メソッドを使用する
+            if (char.IsSurrogatePair(s, index))
+                return char.ConvertToUtf32(s, index);
+
+            return s[index];
+        }
     }
 }
index b3cabe8..5216259 100644 (file)
@@ -1767,7 +1767,7 @@ namespace OpenTween
                     continue;
                 }
 
-                var codepoint = char.ConvertToUtf32(postText, pos);
+                var codepoint = postText.GetCodepointAtSafe(pos);
                 var weight = config.DefaultWeight;
 
                 foreach (var weightRange in config.Ranges)