FIO10-C. rename() 関数の使用に注意する
rename() 関数のプロトタイプは次のとおりである。
int rename(const char *src_file, const char *dest_file);
dest_file が指すファイルが rename() の呼び出し前に存在している場合、動作は処理系定義となる。POSIX システムでは既存ファイルが削除される。Windows システムでは rename() の操作は失敗する。
これは、可搬性のあるコードを書こうとしているときや、別の動作を実装しようとしているときに問題となる。
変更先の名前の既存ファイルを保護したい場合
既存ファイルが消去されたり上書きされないようにする必要がある場合、POSIX 向けのコードには保護策を追加する必要がある。
違反コード (POSIX)
以下のコード例は、変更先の名前の既存ファイルが rename() によって削除される。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (rename(src_file, dest_file) != 0) { /* エラー処理 */ }
適合コード (POSIX)
プログラマの意図が、変更先の名前の既存ファイルを削除しないことである場合は、POSIX の access() 関数を使用してファイルの有無を検査できる [Open Group 04]。以下の解決法は、変更先の名前のファイルが存在しない場合に限り、変更元のファイルの名前を変更する。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (access(dest_file, F_OK) != 0) { if (rename(src_file, dest_file) != 0) { /* エラー条件の処理 */ } } else { /* ファイルが存在する場合の処理 */ }
このコードには、access() の呼び出しと rename() の呼び出しの間に避けられない競合状態が存在するため、セキュアなディレクトリで実行する場合に限り安全である(「FIO15-C. ファイル操作はセキュアなディレクトリで行われることを保証する」を参照)。
指定したディレクトリ内でファイルを閲覧する権限がプログラムに与えられていないファイルシステム上では、ファイルが存在していても access() は -1 を返すことがある。その場合、プログラムには操作を実行する権限が与えられないため、rename() の操作も失敗する。
適合コード (Windows)
Windows では、以下の場合に rename() 関数が失敗する [MSDN]。
newname で指定されたファイルまたはディレクトリがすでに存在する、または作成できない(無効なパス)
したがって、rename() を呼び出す前に対象ファイルの存在を明示的に検査する必要はない。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (rename(src_file, dest_file) != 0) { /* エラー処理 */ }
変更先の名前の既存ファイルを削除したい場合
rename() 操作によって変更先の名前の既存ファイルを確実に削除する必要がある場合は、Windows プログラマはコードを追加する必要がある。
違反コード (Windows)
プログラマの意図が dest_file によって参照されるファイルが存在する場合にそのファイルを削除することである場合、Windows では rename() の操作は失敗するため、以下のコード例は意図どおりの動作にならない。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (rename(src_file, dest_file) != 0) { /* エラー処理 */ }
適合コード (Windows)
Windows システムでは、既存ファイルを上書きして rename() の操作を成功させたい場合は、rename() を呼び出す前に既存ファイルを明示的に削除する必要がある。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (_access_s(dest_file, 0) == 0) { if (remove(dest_file) != 0) { /* エラー条件の処理 */ } } if (rename(src_file, dest_file) != 0) { /* エラー条件の処理 */ }
このコードには _access_s() の呼び出しと remove() および rename() の呼び出しの間に避けられない競合状態が存在するため、セキュアなディレクトリで実行する場合に限り安全である(「FIO15-C. ファイル操作はセキュアなディレクトリで行われることを保証する」を参照)。
適合コード (POSIX)
POSIX システムでは、 rename() の呼び出し前に変更先の名前のファイルが存在する場合は、ファイルが自動的に削除される。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (rename(src_file, dest_file) != 0) { /* エラー条件の処理 */ }
可搬性のある動作
C99 のどの処理系においてもアプリケーションに同じ動作をさせる必要がある場合は、まず実装する動作を決める必要がある。
適合コード (変更先の名前の既存ファイルを削除したい場合)
以下の解決法では、変更先の名前の既存ファイルはすべて、処理系に関係なく削除される。
const char *src_file = /* ... */; const char *dest_file = /* ... */; (void)remove(dest_file); if (rename(src_file, dest_file) != 0) { /* エラー条件の処理 */ }
このコードには、remove() の呼び出しと rename() の呼び出しの間に避けられない競合状態が存在するため、セキュアなディレクトリで実行する場合に限り安全である(「FIO15-C. ファイル操作はセキュアなディレクトリで行われることを保証する」を参照)。
ファイルが存在しない場合は失敗すると想定しているため、remove() の返り値はあえて検査していない。ファイルは存在するが削除できない場合は、rename() の呼び出しも失敗し、その時点でエラーが検出される。これは、レコメンデーション「EXP12-C. 関数の返り値を無視しない」の例外に該当する(EXP12-EX1)。
適合コード (変更先の名前の既存ファイルを維持したい場合)
以下の解決法は変更先の名前のファイルが存在しない場合に限り、変更元のファイルの名前を変更する。
const char *src_file = /* ... */; const char *dest_file = /* ... */; if (!file_exists(dest_file)) { if (rename(src_file, dest_file) != 0) { /* エラー条件の処理 */ } } else { /* ファイルが存在する場合の処理 */ }
このコードには、file_exists() の呼び出しと rename() の呼び出しの間に避けられない競合状態が存在するため、セキュアなディレクトリで実行する場合に限り安全である(「FIO15-C. ファイル操作はセキュアなディレクトリで行われることを保証する」を参照)。
file_exists() 関数はアプリケーションによって提供されプラットフォームによって異なる実装が必要であるため、ここでは記載していない(POSIX システムでは access() を、Windows では _access_s() を、その他のプラットフォームではファイルの有無を調べるための適切な関数を使用)。
リスク評価
新しいファイル名が既存のファイルを指している場合、rename() の呼び出しによる動作は処理系定義となる。rename() の間違った用法によって、ファイルが誤って上書きされたり、その他の予期せぬ動作が発生することがある。
レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
FIO10-C | 中 | 中 | 中 | P8 | L2 |
参考情報
- [ISO/IEC 9899:1999] Section 7.9.4.2, "The rename function"
- [MSDN] rename()
- [Open Group 04] access()
翻訳元
これは以下のページを翻訳したものです。