在 Windows 使用 GPG 加解密及簽章,一般建議用 Gpg4Win,版本較新支援度較好,但有個小問題是 Gpg4Win 所附的 gpg 2.4.3 版,在中文版 Windows 的日期時間顯示會出現亂碼:

Git for Windows 或 Cmder 附的 gpg 2.2.29 版就沒這個問題:

直覺跟 POSIX 語系環境變數有關,但試過設定 LANG、LC_ALL、LC_TIME切到不同語系,但完全無法影響顯示結果。

GPG 是個開源專案,沒有查不出原因的道理,於是 C 語言麻瓜展開冒險,準備追進原始碼找 Bug。

不得不說 Github 的程式碼瀏覽介面變強許多,會自動識別程式語法建立函式來源關聯,追程式碼方便許多。我先在 gnupg Github 專案 搜尋 "Signature made" 找到 mainproc.c 的這段

  if (issuer_fpr)
    {
      log_info (_("Signature made %s\n"), asctimestamp(sig->timestamp));
      log_info (_("               using %s key %s\n"),
                astr? astr: "?", issuer_fpr);

    }
  else if (!keystrlen () || keystrlen () > 8)
    {
      log_info (_("Signature made %s\n"), asctimestamp(sig->timestamp));
      log_info (_("               using %s key %s\n"),
                astr? astr: "?", keystr(sig->keyid));
    }

由程式得知時間顯示文字來自 asctimestamp() 函式,點選 asctimestamp 函式名稱,Github 馬上指出程式碼在 common/gettime.c:

從 asctimestamp() 很快找到以下這段邏輯

#  if HAVE_W32_SYSTEM
  {
    static int done;

    if (!done)
      {
        /* The locale names as used by Windows are in the form
         * "German_Germany.1252" or "German_Austria.1252" with
         * alternate names similar to Unix, e.g. "de-DE".  However
         * that is the theory.  On current Windows and Mingw the
         * alternate names do not work.  We would need a table to map
         * them from the short names as provided by gpgrt to the long
         * names and append some code page.  For now we use "" and
         * take the locale from the user's system settings.  Thus the
         * standard Unix envvars don't work for time and may mismatch
         * with the string translations.  The new UCRT available since
         * 2018 has a lot of additional support but that will for sure
         * break other things.  We should move to ISO strings to get
         * rid of such problems.  */
        setlocale (LC_TIME, "");
        done = 1;
        /* log_debug ("LC_ALL  now '%s'\n", setlocale (LC_ALL, NULL)); */
        /* log_debug ("LC_TIME now '%s'\n", setlocale (LC_TIME, NULL)); */
      }
  }
#  endif
   /* FIXME: we should check whether the locale appends a " %Z" These
    * locales from glibc don't put the " %Z": fi_FI hr_HR ja_JP lt_LT
    * lv_LV POSIX ru_RU ru_SU sv_FI sv_SE zh_CN.  */
  strftime (buffer, DIM(buffer)-1, "%c %Z", tp);
# endif

依程式碼跟註,程式在 Windows 環境下會將 LC_TIME 設成 "",讓內建函式 strftime() 依 Windows 的國別設定決定語系及日期時間格式。

按下 Blame 功能,可追到這是 3 年前 Commit 加入的邏輯:

點擊 Commit 查看詳細資訊,確認這是 2.3 加入的功能。

至此真相大白,這合理解釋了為什麼 2.4 版有亂碼且對 LC_TIME 設定刀槍不入(因為在 Windows 會強制將 LC_TIME 覆寫成 "",改吃 Windows 設定),2.2 版還沒加入這段,故表現正常。

為什麼 LC_TIME 設 "" 用 Windows 設定會出亂碼,這串亂碼原本是什麼中文內容?要繼續探究,少不了寫段 C 實測對照。

想想,其實我也不算完全沒能力寫 C,之前整理過 Windows VSCode C/C++ 開發環境安裝指令懶人包,寫幾行程式重現問題應該難不倒我,更何況現在我有 Github Copilot,GO!! (大推:讓會讀心術的 Github Copilot 陪你寫程式)

來問問 Github Copilot。

點擊左側的 Copilot Chat 圖示,問 Github Coplilot 怎麼在 mingw 呼叫 strftime 顯示現在時間,Copilot 沒酸我不做功課連成天只會伸手,奉上程式範例附貼心解說。

稍加修改,分別設定 LC_TIME 為 "zh_TW.UTF-8" 及 "" 後測試 strftime(),謎團解開:

�U�� 是下午 �x�_�зǮɶ� 是台北標準時間

strftime() 在中文語系 Windows 遇到 LC_TIME = "",會出現中文無法顯示,若設定 "zh_TW.UTF-8" 則正常。爬文發現同樣的問題在 PHP 也能重現,依據微軟文件,在 Windows 10 1803 版之後,呼叫 setlocale(LC_TIME, ".UTF8") 啟用 UTF-8 模式應為正解。

但 GnuPG 有自己的 Bug Tracking System,晚點再設法登入通報 Issue,先列為已知問題避開。

這回成功追到 C 程式的 Bug,還能重現問題,算是項小小突破,證明老狗還是能學會新把戲滴,幫自己拍拍手。(其實大部分是 Github 及 Github Copilot 的功勞啦 😄)


Comments

# by Jian

但覺得是 Windows 處理 setlocale (LC_TIME, ""); 的問題勒

# by Jeffrey

to Jian, C 語言處理 Unicode/UTF-8 有不少眉角。文章的微軟文件連結那篇有提,要啟用 UTF-8 模式,請使用 ".UTF8" 作為 setlocale 時的字碼頁。 例如, setlocale(LC_ALL, ".UTF8") 針對地區設定使用目前的預設 Windows ANSI 字碼頁 (ACP) ,並將 UTF-8 用於字碼頁。

Post a comment