[インデックス 18086] ファイルの概要
このコミットは、Go言語のos
パッケージにおけるPlan 9オペレーティングシステム向けのファイルリネーム(rename
)処理の挙動を修正するものです。具体的には、Plan 9のrename
システムコールが同一ディレクトリ内でのみ機能するという特性に対応するため、クロスディレクトリのリネームを検出して拒否するロジックが追加されています。また、エラーハンドリングにおいてPathError
からLinkError
への変更も行われています。
コミット
os: rename only works as part of the same directory on Plan 9
このコミットメッセージは、Plan 9におけるrename
システムコールの制約、すなわち「同一ディレクトリ内でのみ機能する」という点を明確に示しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/16dcef80d46d78cc198bfc680e9cf05cea9095cc
元コミット内容
commit 16dcef80d46d78cc198bfc680e9cf05cea9095cc
Author: David du Colombier <0intro@gmail.com>
Date: Thu Dec 19 21:20:03 2013 +0100
os: rename only works as part of the same directory on Plan 9
R=golang-dev, lucio.dere, rsc
CC=golang-dev
https://golang.org/cl/44080046
---
src/pkg/os/file_plan9.go | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/pkg/os/file_plan9.go b/src/pkg/os/file_plan9.go
index 278fae772c..102ad5f892 100644
--- a/src/pkg/os/file_plan9.go
+++ b/src/pkg/os/file_plan9.go
@@ -6,6 +6,7 @@ package os
import (
"runtime"
+ "strings"
"syscall"
"time"
)
@@ -314,6 +315,15 @@ func Remove(name string) error {
}
func rename(oldname, newname string) error {
+\tdirname := oldname[:strings.LastIndex(oldname, "/")+1]
+\tif strings.HasPrefix(newname, dirname) {
+\t\tnewname = newname[len(dirname):]
+\t}
+\n+\t// If newname still contains slashes after removing the oldname
+\t// prefix, the rename is cross-directory and must be rejected.
+\t// This case is caught by d.Marshal below.
+\n \tvar d syscall.Dir
+\n \td.Null()
@@ -322,10 +332,10 @@ func rename(oldname, newname string) error {\n \tbuf := make([]byte, syscall.STATFIXLEN+len(d.Name))\n \tn, err := d.Marshal(buf[:])\n \tif err != nil {\n-\t\treturn &PathError{\"rename\", oldname, err}\n+\t\treturn &LinkError{\"rename\", oldname, newname, err}\n \t}\n \tif err = syscall.Wstat(oldname, buf[:n]); err != nil {\n-\t\treturn &PathError{\"rename\", oldname, err}\n+\t\treturn &LinkError{\"rename\", oldname, newname, err}\n \t}\n \treturn nil
}\n```
## 変更の背景
この変更の背景には、Plan 9オペレーティングシステムのファイルシステムにおける`rename`システムコールの特性があります。一般的なUnix系システムでは、`rename`は異なるディレクトリ間でのファイル移動(リネーム)をサポートしていますが、Plan 9の`rename`は同一ディレクトリ内でのファイル名の変更に限定されています。
Go言語の`os`パッケージは、様々なオペレーティングシステムに対応するための抽象化レイヤーを提供しています。そのため、各OSのシステムコールの挙動の違いを吸収し、一貫したインターフェースを提供する必要があります。このコミット以前は、Plan 9向けの`rename`実装が、このOS固有の制約を適切に扱えていなかった可能性があります。その結果、クロスディレクトリのリネームが意図せず成功したかのように見えたり、予期しないエラーが発生したりする問題があったと考えられます。
このコミットは、Plan 9の`rename`のセマンティクスにGoの`os.Rename`関数をより厳密に合わせることで、移植性と堅牢性を向上させることを目的としています。
## 前提知識の解説
### Plan 9
Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの設計思想をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルとして表現し、ファイルシステムを通じてアクセスするという「すべてはファイルである」という原則を徹底しています。
Plan 9のファイルシステムは、そのシンプルさと一貫性で知られています。`rename`システムコールは、このファイルシステムの一部であり、その挙動はUnix系OSとは異なる場合があります。特に、Plan 9の`rename`は、ファイルが移動するのではなく、既存のファイル名が新しいファイル名に「置き換えられる」というセマンティクスを持ち、これが同一ディレクトリ内での操作に限定される理由の一つです。
### Go言語の`os`パッケージ
Go言語の標準ライブラリである`os`パッケージは、オペレーティングシステムとの基本的な相互作用を提供します。これには、ファイル操作(作成、読み書き、削除、リネーム)、ディレクトリ操作、プロセス管理、環境変数へのアクセスなどが含まれます。`os`パッケージは、異なるOS間での挙動の違いを抽象化し、開発者がプラットフォームに依存しないコードを書けるように設計されています。しかし、OS固有の制約がある場合、それを適切にハンドリングするためのプラットフォーム固有の実装(例: `file_plan9.go`)が必要になります。
### `syscall`パッケージ
`syscall`パッケージは、Goプログラムから低レベルのオペレーティングシステムコールに直接アクセスするための機能を提供します。これは、OS固有の機能を利用したり、パフォーマンスが重要な場面でシステムコールを直接呼び出したりする場合に用いられます。このコミットでは、Plan 9の`syscall.Wstat`関数が使用されており、これはファイルのメタデータ(統計情報)を変更するためのシステムコールです。
### `PathError`と`LinkError`
Go言語の`os`パッケージでは、ファイル操作中に発生したエラーを表現するために特定のエラー型が定義されています。
* **`PathError`**: 単一のパス名に関連する操作(例: `os.Remove`)でエラーが発生した場合に使用されます。エラーメッセージには、操作名、関連するパス、および元のエラーが含まれます。
* **`LinkError`**: 2つのパス名に関連する操作(例: `os.Rename`、`os.Link`)でエラーが発生した場合に使用されます。エラーメッセージには、操作名、古いパス、新しいパス、および元のエラーが含まれます。
このコミットでは、`rename`操作が2つのパス(`oldname`と`newname`)を扱うため、より適切なエラー型である`LinkError`に変更されています。これにより、エラー発生時にどのパスが関与していたかをより明確に伝えることができます。
## 技術的詳細
このコミットは、`src/pkg/os/file_plan9.go`ファイル内の`rename`関数に焦点を当てています。
1. **クロスディレクトリリネームの検出**:
* `oldname`からディレクトリ名(`dirname`)を抽出します。これは、`strings.LastIndex(oldname, "/")`を使って最後のスラッシュの位置を見つけ、それまでの部分を切り出すことで行われます。
* `newname`が`dirname`で始まる場合、つまり`oldname`と`newname`が同じディレクトリを共有している可能性がある場合、`newname`から`dirname`のプレフィックスを削除します。これにより、`newname`が相対パスになります。
* プレフィックスを削除した後の`newname`にまだスラッシュが含まれている場合、それはクロスディレクトリのリネームを意味します。Plan 9の`rename`はこれをサポートしないため、このケースは拒否されるべきです。
* この拒否は、後続の`d.Marshal`(`syscall.Dir`構造体をバイト列に変換する処理)によって捕捉されるとコメントで説明されています。これは、`syscall.Dir`構造体がファイル名にスラッシュを含むことを許可しないため、不正なパスとして扱われることを示唆しています。
2. **エラー型の変更**:
* `syscall.Wstat`または`d.Marshal`の呼び出しでエラーが発生した場合、以前は`PathError`を返していました。
* このコミットでは、`rename`操作が`oldname`と`newname`という2つのパスを扱うため、よりセマンティックに適切な`LinkError`を返すように変更されました。`LinkError`は、操作名、古いパス、新しいパス、および元のエラーを含むため、デバッグ時に役立つ情報を提供します。
この変更により、Plan 9上でGoの`os.Rename`を使用する際に、クロスディレクトリのリネームが正しく拒否され、より適切なエラー情報が提供されるようになります。
## コアとなるコードの変更箇所
```diff
--- a/src/pkg/os/file_plan9.go
+++ b/src/pkg/os/file_plan9.go
@@ -6,6 +6,7 @@ package os
import (
"runtime"
+ "strings"
"syscall"
"time"
)
@@ -314,6 +315,15 @@ func Remove(name string) error {
}
func rename(oldname, newname string) error {
+\tdirname := oldname[:strings.LastIndex(oldname, "/")+1]
+\tif strings.HasPrefix(newname, dirname) {
+\t\tnewname = newname[len(dirname):]
+\t}
+\n+\t// If newname still contains slashes after removing the oldname
+\t// prefix, the rename is cross-directory and must be rejected.
+\t// This case is caught by d.Marshal below.
+\n \tvar d syscall.Dir
+\n \td.Null()
@@ -322,10 +332,10 @@ func rename(oldname, newname string) error {\n \tbuf := make([]byte, syscall.STATFIXLEN+len(d.Name))\n \tn, err := d.Marshal(buf[:])\n \tif err != nil {\n-\t\treturn &PathError{\"rename\", oldname, err}\n+\t\treturn &LinkError{\"rename\", oldname, newname, err}\n \t}\n \tif err = syscall.Wstat(oldname, buf[:n]); err != nil {\n-\t\treturn &PathError{\"rename\", oldname, err}\n+\t\treturn &LinkError{\"rename\", oldname, newname, err}\n \t}\n \treturn nil
}\n```
## コアとなるコードの解説
変更は`rename`関数内で行われています。
1. **`import "strings"`の追加**:
`strings.LastIndex`や`strings.HasPrefix`を使用するために、`strings`パッケージがインポートされています。
2. **クロスディレクトリリネームのチェックロジック**:
```go
dirname := oldname[:strings.LastIndex(oldname, "/")+1]
if strings.HasPrefix(newname, dirname) {
newname = newname[len(dirname):]
}
// If newname still contains slashes after removing the oldname
// prefix, the rename is cross-directory and must be rejected.
// This case is caught by d.Marshal below.
```
* `dirname := oldname[:strings.LastIndex(oldname, "/")+1]`:`oldname`の最後のスラッシュまでの部分(ディレクトリパス)を抽出します。例えば、`oldname`が`/a/b/file.txt`であれば、`dirname`は`/a/b/`になります。
* `if strings.HasPrefix(newname, dirname)`:`newname`が`oldname`と同じディレクトリパスで始まるかどうかをチェックします。
* `newname = newname[len(dirname):]`:もし同じディレクトリパスで始まるなら、`newname`からその共通のディレクトリパス部分を削除し、ファイル名のみ(または相対パス)にします。
* コメントにあるように、この処理の後も`newname`にスラッシュが含まれている場合(例: `newname`が`/a/c/newfile.txt`で`dirname`が`/a/b/`の場合、`newname`は変更されずスラッシュを含む)、それはクロスディレクトリのリネームであり、Plan 9では許可されません。この不正なパスは、後続の`syscall.Dir`の`Marshal`処理でエラーとして捕捉されることが期待されています。
3. **エラー型の変更**:
```go
if err != nil {
return &LinkError{"rename", oldname, newname, err}
}
// ...
if err = syscall.Wstat(oldname, buf[:n]); err != nil {
return &LinkError{"rename", oldname, newname, err}
}
```
`d.Marshal`と`syscall.Wstat`の呼び出しでエラーが発生した場合に、`PathError`の代わりに`LinkError`を返すように修正されています。これにより、エラーメッセージに`oldname`と`newname`の両方が含まれるようになり、デバッグ情報が豊富になります。
## 関連リンク
* Go言語の`os`パッケージのドキュメント: [https://pkg.go.dev/os](https://pkg.go.dev/os)
* Go言語の`syscall`パッケージのドキュメント: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
* Plan 9 from Bell Labs: [https://9p.io/plan9/](https://9p.io/plan9/)
## 参考にした情報源リンク
* Go言語のソースコード(`src/pkg/os/file_plan9.go`)
* Go言語の`PathError`と`LinkError`に関するドキュメントや議論
* Plan 9のファイルシステムと`rename`システムコールに関する情報(一般的なOSのドキュメントや論文)
* Go言語のコードレビューシステム(Gerrit)の変更リスト: [https://golang.org/cl/44080046](https://golang.org/cl/44080046) (コミットメッセージに記載)
* Go言語のIssueトラッカー(関連するバグ報告や議論がある場合)
* Stack Overflowや技術ブログなど、Go言語のエラーハンドリングやPlan 9のファイルシステムに関する情報