filepath.EvalSymlinks のバグを修正した
macOS 環境で filepath.EvalSymlinks が想定外のエラーを返すケースがあったので、これについて調べてみた。
% go version
go version go1.19.5 darwin/amd64
% pwd
/Users/matsuyoshi/tmp
% ls -l dst
lrwxr-xr-x 1 matsuyoshi staff 8 1 19 00:28 dst@ -> /tmp/src
% cat main.go
package main
import (
"fmt"
"path/filepath"
)
func main() {
v, err := filepath.EvalSymlinks("dst")
fmt.Println(v)
fmt.Println(err)
}
% go run main.go
lstat private: no such file or directory
処理を追う
GoDoc のコメントからコピペ。
EvalSymlinks returns the path name after the evaluation of any symbolic links. If path is relative the result will be relative to the current directory, unless one of the components is an absolute symbolic link. EvalSymlinks calls Clean on the result.
EvalSymlinks は、引数に受け取ったシンボリックリンクを評価したあとのパスを返す。引数 path が相対パスの場合、コンポーネントの一つが絶対パスへのシンボリックリンクでなければ、結果も現在のディレクトリからみた相対的なものになる。 EvalSymlinks は結果に対して Clean を呼び出す。リンクを辿っていくときに絶対パスのものがあれば結果は絶対パスになる。
実際の処理は walkSymlinks が行っている。処理の概観は以下の通り。
- 引数の path をパスセパレータを区切りとして先頭から評価(評価対象 dest)
-
/a/b/c/d
の場合は/a
を評価→/a/b
を評価→…
-
- dest について os.Lstat を呼び出して FileInfo を取得
- dest がリンクでなくてディレクトリの場合は次のイテレーションに進む(ディレクトリを掘っていく)
- dest がリンクの場合は os.Readlink でリンク先を取得
- リンク先が絶対パスの場合、次のイテレーションで評価する dest を絶対パスとして構築する
- リンク先が相対パスの場合、評価した dest と取得したリンクを元に次のイテレーションで評価する dest を準備する(dest の最後のパスセパレータ以降の部分を相対パスのリンクを使って置き換える)
想定外の no such file or directory
ルートディレクトリ直下に相対パスのリンクがある場合、評価が失敗する。たとえば以下のケース。
- ルートディレクトリ直下にはリンク
a
とディレクトリb
が存在する -
/a
がb/aa
へのリンク(相対パス)になっている
このとき、path に /a/c
へのリンクである d
を指定して filepath.EvalSymlinks を呼び出すと、以下の順序で処理される。
-
d
の評価- dest は
d
-
os.Lstat, os.Readlink により link =
/a/c
を取得して path =/a/c
に更新 - リンクが絶対パスのため、dest を
/
に更新して次イテレーションに進む
- dest は
-
/a/c
の評価- dest は
/a
-
os.Lstat, os.Readlink により link =
b/aa
を取得して path =b/aa
に更新 - リンクが相対パス
-
for r = len(dest) - 1; r >= volLen; r--
のループ - dest は
/a
なので r は 0 - r も volLen も 0 なので
r < volLen
は false - dest が dest[:0] つまり空文字に更新されて次イテレーションに進む
-
- dest は
-
b/aa
の評価- dest は
b
- b というファイルパスが存在せず os.Lstat でエラー
lstat b: no such file or directory
になる
- dest は
macOS では etc
, tmp
および var
は、ルート直下の private
ディレクトリの相対パスのシンボリックリンクになっている。
lrwxr-xr-x@ 1 root wheel 11 10 13 15:06 etc@ -> private/etc
drwxr-xr-x 6 root wheel 192 11 12 01:15 private/
lrwxr-xr-x@ 1 root wheel 11 10 13 15:06 tmp@ -> private/tmp
lrwxr-xr-x@ 1 root wheel 11 10 13 15:06 var@ -> private/var
上記であげた例と対応させると、 a(aa)
が etc
, tmp
, var
で b
が private
になる。
記事冒頭や issue に記載の通り、 /tmp
以下のシンボリックリンクを相対パス形式で filepath.EvalSymlinks に渡すと失敗する。(dst
=> d
/ src
=> c
)
% pwd
/Users/matsuyoshi/tmp
% ls -l dst
lrwxr-xr-x 1 matsuyoshi staff 8 1 19 00:28 dst@ -> /tmp/src
% cat main.go
package main
import (
"fmt"
"path/filepath"
)
func main() {
v, err := filepath.EvalSymlinks("dst")
fmt.Println(v)
fmt.Println(err)
}
% go run main.go
lstat private: no such file or directory
原因
walkSymlinks では、リンクが絶対パスを指していたら、次のイテレーションからは dest を絶対パスで構築して処理している。このときに vol と volLen が更新されていないため、以降のイテレーションでルート直下で相対パスを指しているリンクが出てきたとき、dest が正しく構築されずにエラーになる。
つまり、リンク先が絶対パスのものが登場したタイミングで vol と volLen を更新すればよい。このコミットで修正した(レビューによって原因が明確になった)。
その他
ちなみになんでこれに気付いたかというと、mattn/go-zglob を git clone して go test したら失敗していて、なんでなんだろうなと思って調べたから。当たり前だけど、OSS を自分の環境に持ってきてテストするだけで言語のバグ修正に貢献できるチャンスがある。
zglob って **/*
みたいなやつという認識だけど、ググっても関節の画像ばっかで **/*
だよという説明はみつからなかった。由来はなんだろう?
Discussion