upBibTeXが特定の方法でsubstringを使うとフリーズする

upBibTeXが特定の方法でsubstringを使うとフリーズする

- さかい たかし の投稿
返信数: 3
私が確認に使った環境は
macOS 10.13.6上に
HomebrewでインストールしたTeXLive 2018のupbibtex
upBibTeX 0.99d-j0.33-u1.23 (utf8.uptex) (TeX Live 2018)
および、同じコンピュータ上で
texlive-20180414-source.tar.xzをビルドしたものに含まれるupbibtex
(バージョン同じ)
です。

重複がないかどうかは確認したつもりですが、報告済み・修正済みでしたら申し訳ありません。
(少なくとも1.23で修正された問題とは別であるはずです。)

upBibTeXのスタイルファイル(.bst)において、
特定の文字列に対して特定のパラメータでsubstring$を呼び出し、
その結果に対して別の特定のパラメータでsubstring$を呼び出すと、
結果が文字化けします。
さらにその結果にまた別のパラメータでsubstring$を呼び出すと、
upBibTeXがフリーズします。

特定の文字列・パラメータを具体的にいうと、
(何らかの文字)(半角文字)(全角文字)
という文字列(実用的(?)な例でいうと「第1回」)と、
それを左から1文字ずつ切り落とす操作を行うパラメータ
(例えば
"第1回" #4 #200 substring$ #2 #200 substring$
)です。
(途中の値を変数においても再現するため、文字を数えるなどの処理を書くときに困ります。)
例としてaux, bib, bstの3ファイルを示します。

% a.aux
\bibstyle{a}
\bibdata{./a}
\citation{a}

% a.bib
@a{a,}

% a.bst
ENTRY{}{}{}
FUNCTION {a}{}

READ

STRINGS{t}

FUNCTION {begin.bib}
{
"11あ" #2 #200 substring$ #2 #200 substring$ warning$
"11あ" #2 #200 substring$ #2 #200 substring$ #4 #200 substring$ warning$
}
EXECUTE {begin.bib}

%%%%%%%%%%

ここで、1つ目の行の出力は文字化けしますが、
FUNCTION {begin.bib}
{
"11あ" #2 #200 substring$ #2 #200 substring$ write$ newline$
}
などと書き換えてバイナリエディタで確認すると、出力内容が
e3 81 81 82
であることがわかります。
UTF-8で「あ」は
e3 81 82ですから、2バイト目が2回出力されているようです。

そして、pTeX Liveのビルド途中で生成されるC言語のソースファイルをprintfデバッグした結果、
次の動作を確認しました。

(0)
str_pool[]: 文字列プール(スタック)
pool_ptr: ↑のトップ
str_start[]: 文字列プール内の各文字列の開始位置を表す配列
str_ptr: ↑のトップ
multibytelen(first_byte):
first_byteがシングルバイト文字であれば1を返す
first_byteがマルチバイト文字の1バイト目であればその文字のバイト数を返す
first_byteがマルチバイト文字の2バイト目以降であれば-2を返す
そのほかの場合-1を返す
前のsubstring$の結果をそのまま渡すと、
その値がstr_poolのトップに来る。

(1) substring$の冒頭で引数を取り出す。
最初の引数は文字列であるが、このとき、次の処理が行われる。
(a). str_ptr-- (str_startをpop)
(b). pool_ptr = str_start[str_ptr](str_poolにstr_startのトップを代入)

(2) 計算する。
sp_ptr: 指定されたsubstringのstr_poolにおける開始バイト
sp_end: 指定されたsubstringのstr_poolにおける終了バイト+1

(3) substring$の最後で出力の文字列を一文字ずつ書き込む。
書き込みのアルゴリズムは以下の通り
while (sp_ptr < sp_end)
str_pool[pool_ptr++] = str_pool[sp_ptr];
if multibytelen(str_pool[sp_ptr]) > 1 then
str_pool[pool_ptr++] = str_pool[sp_ptr+1];
if multibytelen(str_pool[sp_ptr]) > 2 then
str_pool[pool_ptr++] = str_pool[sp_ptr+2];
if multibytelen(str_pool[sp_ptr]) > 3 then
str_pool[pool_ptr++] = str_pool[sp_ptr+3];
if multibytelen(str_pool[sp_ptr]) > 0 then
sp_ptr := sp_ptr + multibytelen(str_pool[sp_ptr])

2回目のsubstring$のとき、
「前のsubstring$の結果をそのまま渡すと、
その値がstr_poolのトップに来る」ため
(3)では
□123
というデータをin placeで
123空
にすることになります。しかし、このアルゴリズムではそれができません。
まず
str_pool[pool_ptr++] = str_pool[sp_ptr];
if multibytelen(str_pool[sp_ptr]) > 1 then
str_pool[pool_ptr++] = str_pool[sp_ptr+1];
を実行するところまでは行きますが、この結果
__↓← pool_str
1223
_↑← sp_ptr
と、str_pool[sp_ptr]がマルチバイト文字の2バイト目の文字になってしまうため
if multibytelen(str_pool[sp_ptr]) > 2 then
str_pool[pool_ptr++] = str_pool[sp_ptr+2];
if multibytelen(str_pool[sp_ptr]) > 3 then
str_pool[pool_ptr++] = str_pool[sp_ptr+3];
if multibytelen(str_pool[sp_ptr]) > 0 then
sp_ptr := sp_ptr + multibytelen(str_pool[sp_ptr])
までを素通りして
else
sp_ptr := sp_ptr + 1
に行ってしまいます。すると
__↓← pool_str
1223
__↑← sp_ptr
という状況になって、結局
1223
という出力になります。

(1)と(3)のどちらかが間違っているのは確かだと思いますが、
私にはどちらが正しいか判断できませんでした。
・文字をシフトさせる処理をin placeで行うという速度低下につながるメモリ利用
・(引数の値とstr_poolのトップの値が無関係である場合があるにも関わらず)
文字列プールstr_poolのトップを削除するという乱暴な処理
などの点から(1)の方がかなり怪しいと思います。
しかし、修正が楽なのは(3)だと感じたので、試しにパッチを書いてみました。

/* ---------- */

--- a/texk/web2c/uptexdir/upbibtex.ch 2018-02-25 09:31:25.000000000 +0900
+++ b/texk/web2c/uptexdir/upbibtex.ch 2018-09-04 14:51:37.000000000 +0900
@@ -232,6 +232,21 @@
@z

@x
+@<|execute_fn|({\.{substring\$}})@>=
+procedure x_substring;
+label exit;
+var tps,tpe:pool_pointer; {temporary pointer}
+begin
+@y
+@<|execute_fn|({\.{substring\$}})@>=
+procedure x_substring;
+label exit;
+var tps,tpe:pool_pointer; {temporary pointer}
+ mblen_top:integer; {multibyte_length of the character at the top of copy source}
+begin
+@z
+
+@x
{ 2 bytes Kanji code break check }
tps:=str_start[pop_lit3];
while (tps < sp_ptr) do begin
@@ -299,14 +314,15 @@
end;
@y
append_char (str_pool[sp_ptr]);
- if multibytelen(str_pool[sp_ptr]) > 1 then
+ mblen_top := multibytelen(str_pool[sp_ptr]);
+ if mblen_top > 1 then
append_char (str_pool[sp_ptr+1]);
- if multibytelen(str_pool[sp_ptr]) > 2 then
+ if mblen_top > 2 then
append_char (str_pool[sp_ptr+2]);
- if multibytelen(str_pool[sp_ptr]) > 3 then
+ if mblen_top > 3 then
append_char (str_pool[sp_ptr+3]);
- if multibytelen(str_pool[sp_ptr]) > 0 then
- sp_ptr := sp_ptr + multibytelen(str_pool[sp_ptr])
+ if mblen_top > 0 then
+ sp_ptr := sp_ptr + mblen_top
else
incr(sp_ptr);
@z

/* ---------- */

これが適切なパッチか判断して、適切であれば修正して反映などしていただけるとありがたいと思うのですが、
これはどこに言えば良いのでしょうか。

よろしくお願いいたします。
さかい たかし への返信

Re: upBibTeXが特定の方法でsubstringを使うとフリーズする

- aminophen の投稿
とりあえず埋もれないように
https://github.com/texjporg/tex-jp-build/issues/64
からリンクを張っておきました。

バグ修正などの開発に関する話は GitHub の方が(多分)あとあと参照しやすいので better ですが,
こちらの TeX Forum を読んでいる人も多いので,どちらでも大丈夫です。
aminophen への返信

Re: upBibTeXが特定の方法でsubstringを使うとフリーズする

- t tk の投稿
みなさん有り難うございます。
さかいさんから頂いたパッチを取り込ませていただきました。

日本語TeX開発コミュニティ gitHub (#64, #66)
TeX Live svn (r48676)

TeX Live においては TeX Live 2019 に含まれることになります。