生涯未熟

生涯未熟

プログラミングをちょこちょこと。

.zsh_historyにおける非ASCII文字の扱いについて

この3連休、夢中でコーディングしてあるツールを作っている最中、.zsh_historyの特異な挙動を発見しましたので書き残しておきます。

一体何が起こったのか?

Goでツールを作っていたのですが、.zsh_historyを読み込んでファイル内容を出力したところ以下のようになりました。

$ go run main.go
vim pre-commit.sample
cp pre-commit.sample pre-commit
vim .git/hooks/pre-commit
vim pre-commit
git commit --allow-empty -m "SideCIぃ�胰�ぃ�ぃ�ぃ�め僦��PR"
view raw gistfile1.txt hosted with ❤ by GitHub

思いっきり文字化けしている・・・
見るに、日本語が文字化けしている様子。

で、あれー?と思ったので.zsh_historyvimで開いてみました。

git commit --allow-empty -m "SideCIã<81><83>¬è<83>°½ã<81><83><81>ã<81><83>¿ã<81><83>¿ã<82><81>å<83>¦<83>­PR"
view raw gistfile1.txt hosted with ❤ by GitHub

???🤔

.zsh_historyの特異な性質を知る

さて、なんでこのように日本語が記述されていないのでしょうか?
調べたところ以下のブログ記事に辿り着きました。

kawabata.github.io

ここに興味深い文言が。

zshのヒストリファイルは、保存するデータの各バイトにおいて、それが 0x80-0x9d または 0xa0 のとき、その前に 0x83 を入れて、続くバイトの 6bit 目を反転させるようになっている。

ほほう?🤔

これがどういうことか、具体的な例を使って見ていきましょう。
例として「あいうえお」という文字列を使います。

この「あいうえお」を.zsh_historyで見ると以下のようになります。

ã<81><82>ã<81><83>¤ã<81><83>¦ã<81><83>¨ã<81><83>ª

.zsh_historyはlatin1(ISO/IEC 8859-1)という形式になりますので、その文字コード表を元にすると以下のように16進数に直すことが出来ます。
(分かりやすいように適宜スペースを挿入しています)

E38182 E38183 A4 E38183 A6 E38183 A8 E38183 AA

ここで、 0x80-0x9d または 0xa0 のとき、その前に 0x83 を入れて というのを考えると、上記の 83 に当たる所が該当します。
今回だと、該当するのは A4 , A6 , A8 , AA ですね。

なので、 続くバイトの 6bit 目を反転させるようになっている というルールに従って、6bit目を更に反転させると、
84 , 86 , 88 , 8A になります。

で、このルールに従って書き直すと以下のようになります。

E38182 E38183 84 E38183 86 E38183 88 E38183 8A

そして、更にマーキングに使っている 83 を削除すると

E38182 E38184 E38186 E38188 E3818A

といった形になります。

これを、Unicode文字コード表に照らし合わせると、あら不思議。

あいうえお

と読み解くことが出来ます。

Goだとどうする?

さて、ここまで理解した上で、Goだと以下のようにParse処理を書くことが出来ます。

func parseNonAscii(latin1Byte []byte) string {
isMarking := false
var byteBuffer []byte
for _, codePoint := range latin1Byte {
// 131は0x83の10進数表現
if codePoint == 131 {
isMarking = true
continue
}
if isMarking {
// 6bit目を反転させるために
// 0x20をXORする
invertCodePoint := codePoint ^ 32
byteBuffer = append(byteBuffer, invertCodePoint)
isMarking = false
} else {
byteBuffer = append(byteBuffer, codePoint)
}
}
return string(byteBuffer)
}
view raw parseNonAscii.go hosted with ❤ by GitHub

これに scanner.Scan 等で取得してきた文字列を突っ込むことで、見事に日本語に変換してくれます。

.zsh_historyの特異な性質を知ることが出来、また一つ学びになりました。
何故にこのような処理を非ASCII文字に対して行っているのかは謎なところですが・・・🤔