マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

もう脆弱性なんて怖くない(※ただしLaTeXの意味で)(1)

LaTeX において、ユーザからの自由なテキスト(LaTeX の命令を含みうる)の入力を受け付ける(ToL の)マクロを実装しようとすると、「脆弱(fragile)」なマクロの取扱に関する知識が必要な場合がある。カウンタ出力命令を作る芸人実力判定問題の核心部分の課題もそれである。本記事では、その「核心部分」を例にとって、「脆弱」という性質について解説を行う。

核心部分

問題は、次の要件を満たす命令 \NabeAzzLike を実装することである。((改訂前の形式に戻して、かつ脆弱な \nabeazz の実装があることを前提としている。))

  • \nabeazz{<整数>} が引数の整数(10 進表記に限る)を「ある特定の形式」で出力する命令であると仮定する。ただし、\nabeazz脆弱であるかも知れない。
  • この時、\ NabeAzzLike{<LaTeXカウンタ名>} は当該のカウンタの現在の値を \nabeazz の定める形式で出力するカウンタ出力命令となる。(相互参照が正常に動作する必要がある。)
脆弱性を考えない場合

まずは「脆弱な命令の取扱」については放置して、規定の動作を行う \NabeAzzLike を実装してみよう。概念的には、\NabeAzzLike{<カウンタ名>}\nabeazz{\arabic{<カウンタ名>}} の動作になる必要がある。これをそのまま TeX コードにすると次のようになる。

\def\NabeAzzLike#1{\nabeazz{\arabic{#1}}}

ただ、問題の条件だと、\nabeazz の引数に渡す整数は 10 進表記そのものでないといけない。つまり、\arabic を完全に展開する必要がある。\arabic{enumi} を展開すると \number\csname c@enumi\endcsname になり、それをもう 1 回展開すると数字列になる。((\number を展開すると、整数が得られるまで後続のトークン列が自動的に展開される。この場合、\csname の展開はこの自動展開の中に含まれる。))そこで、\arabic を「先に」展開させるために \expandafter を使うが、面倒を避けるために((つまり、\arabic を先に「2 回」展開させようとすると \expandafter を 3 重にする必要があって、とっても嫌である。))、数字列になる直前の形を使うことにする。すると次のようになる。

\def\NabeAzzLike#1{\expandafter\nabeazz\expandafter{\number\csname c@#1\endcsname}}

これで、相互参照を介さずに直接使用するケースは正しく動く。

\setcounter{enumi}{3} % テスト用にenumiを使う
\LaTeX\NabeAzzLike{enumi}

つまりここでは \nabeazz が「脆弱」であることは問題にならない。そもそも、LaTeX においては脆弱性とは「動く引数の中では正常に動作しない」ことであった。上のソースだと「動く引数」が登場しないので問題にならなかったわけである。そして、この出題で「相互参照」を絡めているのは、それが「動く引数」になる(従って脆弱性に対処する必要が生じる)からであり、実際に相互参照が絡む場合には上記の単純な定義は失敗する。例えば、次のようなソースは意図した出力にならない。 (ここで、tcnabeazz.sty には、出題時の記事の最初のコードの \makeatletter\makeatother の間にある \nabeazz の実装コードが書かれているものとする。((この \nabeazz の実装の場合、エラーにならずにデタラメな数値がカウンタ値として出力されるようである。もちろん、実装によっては何も出力されない、エラーが発生する等の結果もあり得る。)))

[test1.tex]
\documentclass[a4paper]{article}
\usepackage{tcnabeazz}
\makeatletter %!!!!!!!!!!!!!!!!!!!!!!!!!
\def\NabeAzzLike#1{\expandafter\nabeazz\expandafter{\number\csname c@#1\endcsname}}
\makeatother  %!!!!!!!!!!!!!!!!!!!!!!!!!
\begin{document}
\renewcommand{\theenumi}{\NabeAzzLike{enumi}}
Essential packages:
\begin{enumerate}
\item graphicx; \label{gra}
\item hyperref; \label{hyp}
\item iamjatex. \label{iam}
\end{enumerate}
Items \ref{gra}, \ref{hyp}, and \ref{iam}.
\end{document}

この場合は、\NabeAzzLike を「頑強」なマクロとして定義する必要があるからである。

「動く引数」とは結局何なのか

頑強(robust)とは脆弱の論理的否定であるから、要するに「頑強なマクロ」とは「動く引数の中で使われても正常に働くマクロ」ということである。しかし「引数が動く」という概念が具体的に何を意味するのかが判らないと対策の立てられないだろう。ここではそれについて説明する。

非常に粗く言うと、「引数が動く」とは「引数が展開限定文脈で完全展開される(という処理が存在する)」ということである。例えば、相互参照の場合では aux ファイルに参照した番号の情報を書き出す処理*1がありここで \NabeAzzLike が完全展開される。この粗い説明に従うと、頑強とは要するに完全展開可能(これについては本ブログで既に何度も取り上げている)であるということになる。実際にはこれ自体は正しくないが、しかし「マクロが完全展開可能であればそれは頑強である」というのは成立する。((つまり、定義上は「完全展開可能」は「頑強」の特別な場合である。ただし、完全展開可能は非常に重要な性質なので、便宜上「頑強」の中に含めずに扱われることも多い。しかしそうでない例もあり、例えば Lamport 本では \symbol (これは完全展開可能)を「頑強」としている。(実際、純粋な LaTeX では頑強な命令が TeX 的に完全展開可能であるかはあまり重要ではない。)))従って、仮に前掲のコードで \nabeazz が完全展開可能な実装を持っていたならば、\NabeAzzLike(これも完全展開可能、すなわち頑強となる)を用いた相互参照は正常に機能する。

といっても、仮に「頑強」なものが完全展開可能なものしかないとすると、「動く引数」で用いるマクロを全て完全展開可能な形で実装する必要があることになり、その実行は困難を極めるだろう。従って、LaTeX では上述の「完全展開」の際に特別な措置を講じて完全展開可能でないマクロの使用を可能にしている。

*1:これには \write プリミティブが使われ、その引数は展開限定文脈で完全展開される。