nkf を使用した文字コード変換ライブラリCinnamonのUCS4->UTF8の変換

nkf を使用した文字コード変換ライブラリCinnamon についてUCS4⇔UTF8の変換にてバグらしきものを発見したと指摘があります。

指摘されているのは UCS4 の1文字を Utf8 の複数バイトに変換する ucs4CharToUtf8Chars 関数です。

文字列を変換して比較し、ucs4CharToUtf8Chars について検証してみました。

  • まず、rfc2279。
module Main where

import Data.Bits ((.&.), (.|.), shiftR)
import Data.Char (chr, ord)
import Codec.Binary.UTF8.String (encodeString)

{-
  参照: <http://www.ietf.org/rfc/rfc2279.txt>

  UCS-4 range (hex.)    UTF-8 octet sequence (binary)
  0000 0000-0000 007F   0xxxxxxx
  0000 0080-0000 07FF   110xxxxx 10xxxxxx
  0000 0800-0000 FFFF   1110xxxx 10xxxxxx 10xxxxxx

  0001 0000-001F FFFF   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  0020 0000-03FF FFFF   111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  0400 0000-7FFF FFFF   1111110x 10xxxxxx ... 10xxxxxx

-- http://d.hatena.ne.jp/sirocco/20100331/1270027037
Prelude> Numeric.showIntAtBase 2 Char.intToDigit 123 "" -- 2進数文字列に
"1111011"
Prelude> Numeric.showIntAtBase 4 Char.intToDigit 123 "" -- 4進数文字列に
"1323"
Prelude> Numeric.showIntAtBase 16 Char.intToDigit 123 "" -- 16進数文字列に
"7b"
-}

-- 【Cinnamonオリジナル】
-- | UCS4の文字をUTF8の文字に変換します。
-- UTF8の文字は最大Char6文字分で表現されます。
ucs4CharToUtf8Chars :: Char -> [Char]
ucs4CharToUtf8Chars c
  | c' <= 0x0000007f = [c]
  | c' <= 0x000007ff = map chr $ marks 0xc0 $ masks 2
  | c' <= 0x0000ffff = map chr $ marks 0xe0 $ masks 3
  | c' <= 0x1fffffff = map chr $ marks 0xf0 $ masks 4
  | c' <= 0x3fffffff = map chr $ marks 0xf8 $ masks 5
  | c' <= 0x7fffffff = map chr $ marks 0xfc $ masks 6
  | otherwise        = error "ucs4CharToUtf8Char: out of range."
  where
    c' :: Int
    c' = ord c

    mask :: Int -> Int
    mask n = (c' `shiftR` (n * 6)) .&. 0x3f

    masks :: Int -> [Int]
    masks n = map mask $ reverse $ [0 .. n - 1]

    marks :: Int -> [Int] -> [Int]
    marks m0 = zipWith (\ m c -> m .|. c) (m0 : repeat 0x80)

-- | UCS4の文字列をUTF8の文字列に変換します。
ucs4ToUtf8 :: String -> String
ucs4ToUtf8 = concatMap ucs4CharToUtf8Chars

-- 【指摘のあったもの】
-- | UCS4の文字をUTF8の文字に変換します。
-- UTF8の文字は最大Char6文字分で表現されます。
ucs4CharToUtf8Chars2 :: Char -> [Char]
ucs4CharToUtf8Chars2 c
  | c' <= 0x0000007f = map chr $ marks 0x00 $ masks 1 0x7f
  | c' <= 0x000007ff = map chr $ marks 0xc0 $ masks 2 0x3f
  | c' <= 0x0000ffff = map chr $ marks 0xe0 $ masks 3 0x3f
  | c' <= 0x1fffffff = map chr $ marks 0xf0 $ masks 4 0x3f
  | c' <= 0x3fffffff = map chr $ marks 0xf8 $ masks 5 0x3f
  | c' <= 0x7fffffff = map chr $ marks 0xfc $ masks 6 0x3f
  | otherwise        = error "ucs4CharToUtf8Char: out of range."
  where
    c' :: Int
    c' = ord c

    mask :: Int -> Int -> Int
    mask m n = (c' `shiftR` (n * 6)) .&. m

    masks :: Int  -> Int -> [Int]
    masks n m= map (mask m) $ reverse $ [0 .. n - 1]

    marks :: Int -> [Int] -> [Int]
    marks m0 = zipWith (\ m c -> m .|. c) (m0 : repeat 0x80)

ucs4ToUtf8' :: String -> String
ucs4ToUtf8' = concatMap ucs4CharToUtf8Chars2

-- 【utf8-Stringのパクリ】
ucs4CharToUtf8Chars3 :: Char -> [Char]
ucs4CharToUtf8Chars3 c
   | c' <= 0x7f       = [c]                             -- 0x7f  以下(ASCII)は同じ
                                                        -- ラテン補助、拡張、・・・ギリシャ文字、アラビア文字
   | c' <= 0x7ff      = map chr [ 0xc0 + (c' `shiftR` 6)        -- 右へ6ビットシフトした残りと 110B の OR 
                                , 0x80 + c' .&. 0x3f]            -- 甲斐6ビットをマスクし 10B と OR

   -- ひらがな、漢字、ハングルなど。Windows IME 文字パッドはここまで
   | c' <= 0xffff     = map chr [ 0xe0 + (c' `shiftR` 12)              -- 右へ6ビットシフトして1110B と OR
                                , 0x80 + ((c' `shiftR` 6) .&. 0x3f)    -- 右へ6ビットシフトして下位6ビットをマスク
                                                                        --  10B と OR
                                , 0x80 + c' .&. 0x3f]                   -- 下位6ビットをマスクし 10B と OR
   -- それ以外は4バイト表現。5バイト、6バイト表現の文字には対応していない。
   | otherwise        = map chr [ 0xf0 + (c' `shiftR` 18)
                                , 0x80 + ((c' `shiftR` 12) .&. 0x3f)
                                , 0x80 + ((c' `shiftR` 6) .&. 0x3f)
                                , 0x80 + c' .&. 0x3f]
  where
    c' :: Int
    c' = ord c

ucs4ToUtf8'' :: String -> String
ucs4ToUtf8'' = concatMap ucs4CharToUtf8Chars3

-- UCS4 の文字コードは0x7fffffffまでありますがCharのmaxBound は0x10ffffです。
uni :: [Char]
uni = map chr [0x0..0x10ffff]
-- maxBound :: Char '\1114111'
-- ucs.exe: Prelude.chr: bad argument: 1114112
-- Numeric.showIntAtBase 16 Char.intToDigit 1114111 "" --> "10ffff"


main :: IO ()
main = do
  print (ucs4ToUtf8  uni == ucs4ToUtf8'  uni)  -- > True
  print (ucs4ToUtf8  uni == ucs4ToUtf8'' uni)  -- > True
  print (ucs4ToUtf8' uni == encodeString uni)  -- > True

0x0から0x10ffffのUCS4文字列を作りUTF8に変換後の文字列を比較しましたが違いはないようです。

  • 速度の比較
  -- print (length (ucs4ToUtf8  uni))   -- > 0m0.985s
  -- print (length (ucs4ToUtf8'  uni))  -- > 0m0.901s
  -- print (length (ucs4ToUtf8''  uni)) -- > 0m0.381s
  -- print (length (encodeString  uni)) -- > 0m0.286s

速度については、utf8-stringのencodeStringが最速、utf8-string とほぼ同じものが次いで、さらに、修正版、オリジナルとなります。
nkfを用いた文字コード変換ライブラリCinnamon はUCS4からUTF8に変換し、それをnkfで変換しています。UCS4⇔UTF8の部分をencodeString、decodeStringを使用すればコードもすっきりし、高速になります。その変更手順を後日書きます。