Programming Field

Windows の「ごみ箱」の構造

このページでは、Windows で使用されている「ごみ箱」について、解析したデータなどを元に記述したページです。ここに書かれている内容を利用すれば、DOS 上で動く WRecycle のように、Windows 以外でもごみ箱を操作することが出来ます。

ごみ箱

「ごみ箱」は、不要になったファイルを一時的に集約する場所で、ファイルを誤って消すのを多少防止する機能です。

MS-DOS 時代には似たような機能に「Undelete」というものが存在しましたが、ごみ箱と動作は似ているものの、データの扱い方は異なります。

Recycled フォルダ / Recycler フォルダ

Recycled フォルダ / Recycler フォルダは、ごみ箱に捨てられたファイルを保管しておく場所です。Recycled フォルダは FAT や FAT32 ファイルシステムでごみ箱を使用する設定になっているディスク上に、Recycler フォルダは同様に NTFS ファイルシステムに作成されます。なお、ファイルを一度も捨ててない場合は作成されていない場合もあります。

Recycled フォルダには desktop.ini と INFO (または INFO2) ファイルが置かれています。また、ごみ箱にファイルが捨てられた場合、そのファイルが名前を変えて(拡張子はそのままで)このフォルダに移動されます。ただし Recycled フォルダに移動できるファイルは、基本的には Recycled フォルダと同じドライブにあるファイルのみです。

Recycler フォルダは、Recycled フォルダを拡張した構造になっており、Recycler の直下には、「SID」と呼ばれるログインユーザを識別する ID の名前をしたフォルダが置かれています。これらは、ごみ箱のデータがユーザ間で共有されないように作られており、基本的に該当するユーザのみが使用できます。各 SID のフォルダは、それぞれが Recycled フォルダと同様の構造になっています。

レジストリによるごみ箱の制御

※ 以下に書かれているレジストリのデータは、エクスプローラ以外のアプリケーションが値を変更しても、現在実行しているエクスプローラには値が反映されず、ごみ箱のプロパティを改めて表示したり、該当のエクスプローラを再起動する必要があります。

ごみ箱を利用するかどうかの情報や、ごみ箱の最大容量などはレジストリによって制御されています。その場所は HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Explorer\BitBucket です。

BitBucket にはレジストリデータがいくつかあり、さらに「c」や「d」などのサブキーがあります。このアルファベット1文字のキーはドライブ名を表しており、各ドライブごとのごみ箱の設定データをキープしています。

BitBucket 直下のレジストリデータと、各ドライブごとのレジストリのデータは、共通して以下の内容が含まれます。ごみ箱をドライブごとに設定している場合で、ドライブ別のレジストリデータが存在しない場合は、BitBucket 直下のデータを利用します。

NukeOnDelete
「ごみ箱にファイルを移動しないで、削除と同時にファイルを消す」オプションの設定項目です。0 以外の値なら、ごみ箱を使用せずファイルを直接削除します。(DWORD 値)
Percent
「ごみ箱の最大サイズ」オプションの設定項目です。ごみ箱の最大サイズをパーセントで指定します。(DWORD 値)
UseGlobalSettings
[BitBucket のみ] 「全ドライブで同じ設定を使う」オプションの設定項目です。0 以外の値なら全ドライブ共通の設定を使用し、0 なら各ドライブごとの設定を使用します。(DWORD 値)
IsUnicode
[各ドライブ設定のみ] ドライブが NTFS ファイルシステムなどの Unicode ファイル名が使用できるドライブであるかどうかを示します。0 以外の値なら Unicode ファイル名が使用でき、0 または値が存在しない場合は Unicode は使用できません。(DWORD 値)
VolumeSerialNumber
[各ドライブ設定のみ] ドライブのボリュームシリアル番号を示します(あまり使用されていない模様)。(DWORD 値)

※ ごみ箱のプロパティにある「削除の確認メッセージを表示」に当たるレジストリ項目は、上記とは異なる場所にあり、HKEY_CURRENT_USER\Software\Microsoft\Windows\Explorer にある ShellState という値の一部分です。この値はバイナリ形式で、5 バイト目(+04H)の下位3ビット目(& 0x04)のフラグを ON にする(0x04 を OR で加える)と「削除の確認メッセージを表示」しないようになり、OFF にすると表示するようになります。値を変更する際は他のビットを変更しないように注意してください。

INFO ファイル / INFO2 ファイル

INFO (INFO2) ファイルは、ごみ箱に捨てられたファイルを管理するために Recycled (Recycler) フォルダに置かれた隠しファイルです。INFO ファイルは、シェルが更新されていない Windows 95 で使用され、INFO2 ファイルはそれ以外の Windows 95 以降の OS で使用されています(NT 4.0 あたりは未確認)。ファイル名は異なりますが、内部の構造はほとんど同じです。

INFO (INFO2) ファイルのデータは、以下の構造体から始まっています。

※ 構造体名やメンバ名はすべて自分が勝手に付けた名前です。

[C/C++]

struct CRecycleInfoHeader {
    DWORD dwVersion;
    DWORD dwCount;
    DWORD dwNextIndex;
    DWORD dwRecordSize;
    DWORD dwAllSize;
};

[VB.NET]

Structure CRecycleInfoHeader
    Dim dwVersion As UInteger
    Dim dwCount As UInteger
    Dim dwNextIndex As UInteger
    Dim dwRecordSize As UInteger
    Dim dwAllSize As UInteger
End Structure
dwVersion
INFO (INFO2) ファイル形式のバージョンです。INFO の場合は 1、INFO2 の場合は 4 や 5 などです。
dwCount
[INFO ファイルのみ] 現在ごみ箱に捨てられているファイルの総数です。エクスプローラは初めてごみ箱を利用するときにこの値をチェックするため、たとえファイルが捨てられていてもこの値が 0 になっている場合は、ごみ箱が空っぽであると誤認してしまいます。
INFO2 ファイルでは、このメンバは 0 に設定されています。
dwNextIndex
[INFO ファイルのみ] 後述するリネームされたファイルに付けるべきである次の番号です。エクスプローラは初めてごみ箱を利用するときにこの値をチェックするため、ごみ箱内に既に存在する番号の値がここにセットされている場合、エクスプローラ上で新たにファイルが捨てられると、重複する番号のファイルが上書きされて無くなってしまいます。
INFO2 ファイルでは、このメンバは 0 に設定されています。
dwRecordSize
この構造体に続く、捨てられたファイルの情報を持つ構造体のサイズです。この値は固定値では無い(Recycled では 0x118、Recycler では 0x320 など)ので、互換性を考える場合はこの値をチェックする必要があります。
dwAllSize
ごみ箱に捨てられたファイルすべてのディスク上のサイズです。この値には desktop.ini や INFO (INFO2) ファイルのサイズは含みません。

上の構造体に続いて、捨てられたファイルの情報を持つ次の構造体が、ファイルの数だけ続きます。

[C/C++]

struct CRecycleInfoRecord {
    char szPath[260];
    DWORD dwIndex;
    DWORD dwDriveNumber;
    FILETIME ftDeleted;
    DWORD dwSizeOnDisk;
};
/* sizeof(CRecycleInfoRecord) == 0x118 */

[VB.NET]

Structure CRecycleInfoRecord
    Dim szPath As String
    Dim dwIndex As UInteger
    Dim dwDriveNumber As UInteger
    Dim ftDeleted As Long
    Dim dwSizeOnDisk As UInteger
End Structure

※ VB.NET では固定長の文字列メンバを書くことが出来ないので、この構造体を直接ファイル入出力に用いることは出来ません。

szPath
ごみ箱に移す前の(元の)ファイル名です。ファイル名の長さは MAX_PATH マクロと同様の 260 文字までで、それより短い場合は末尾に NULL 文字を追加します。
dwIndex
後述する、リネームされたファイルに付けられた番号です。ごみ箱からファイルを元に戻す場合、この番号と一致するファイルが使用されます。
dwDriveNumber
元のファイルが存在していたドライブの文字を番号で表したものです。0 なら A ドライブ、2 なら C ドライブ、という感じです。
ftDeleted
ファイルが捨てられた日時を表す FILETIME 構造体です。
[VB.NET] System.DateTime 構造体に直すには、System.DateTime.FromFileTime メソッドを使用します。
dwSizeOnDisk
ごみ箱に捨てられたファイルのディスク上のサイズです。

NTFS ファイルシステムのごみ箱 (Recycler) では、CRecycleInfoHeader::dwRecordSize の値が 0x320 です。残りの 0x208 (520) バイトは、ファイル名を Unicode 文字で表したものが入ります。

[C/C++]

struct CRecycleInfoRecord2 {
    char szPath[260];
    DWORD dwIndex;
    DWORD dwDriveNumber;
    FILETIME ftDeleted;
    DWORD dwSizeOnDisk;
    wchar_t szPath2[260];
};
/* sizeof(CRecycleInfoRecord2) == 0x320 */

リネームされたファイル

ファイルがごみ箱に捨てられると、ファイルが重複しないようにリネームされます。その名前は以下の規則に従っています。

  1. ファイル名は必ず「D」で始まります。
  2. 2文字目は、ファイルが存在していた元のドライブの文字を小文字で表したものです(C ドライブなら 'c')。
  3. 次に来るのは、重複を避けるために付けられたインデックス番号で、0 から始まります。ただし、重複せずに CRecycleInfoRecord::dwIndex と一致していれば、どんな番号でも構いません。
  4. 残りは拡張子が付きます。拡張子は元のファイルと同じものが付けられます(「Hello.txt」なら「.txt」、「Files.tar.gz」なら「.gz」)。捨てられたものがフォルダ(ディレクトリ)の場合も、「.」が含まれていれば、それ以降が拡張子と見なされます。

なお、捨てられたファイルに付けられた属性(隠しファイルなど)や所有者情報(NTFS のみ)は、「ファイルをリネームしているだけ」なのでそのまま残ります。

これに従うと、ファイル「D:\Files\Data.Bin」がごみ箱に捨てられた場合、番号が 3 だと「Dd3.Bin」というファイル名になります。

ごみ箱のデータを扱うのに役に立つ関数

ごみ箱のデータを扱うには、「FILETIME 構造体」や、「ディスク上のサイズ」などが必要になってきます。これらを DOS 上で使える関数を使って計算するには、以下のようになります。

[C/C++]

typedef struct _FILETIME
{
    DWORD dwLowDateTime;
    DWORD dwHighDateTime;
} FILETIME;

/* FILETIME から time_t に変換する関数 */
time_t WinFileTimeToTimeT(const FILETIME* pFileTime, time_t* ptm)
{
    __int64 n64;
    n64 = *((__int64*) pFileTime);
    n64 -= (11644473600 * 10000000);
    /* ここで一部の時間が削られる */
    n64 /= 10000000;
#if _MSC_VER >= 1400 && !defined(_USE_32BIT_TIME_T)
    /* time_t が 64 ビット値の場合 */
    if (ptm)
        *ptm = (time_t) (n64);
    return (time_t) (n64);
#else
    /* time_t が 32 ビット値の場合 */
    if (ptm)
        *ptm = (time_t) (n64 & 0xFFFFFFFF);
    return (time_t) (n64 & 0xFFFFFFFF);
#endif
}

/* time_t から FILETIME に変換する関数 */
void TimeTToWinFileTime(time_t timeData, FILETIME* pFileTime)
{
    __int64 n64;
    n64 = ((__int64) timeData + 11644473600) * 10000000;
    /* nsec = ナノ秒とすると、n64 += (nsec / 100) が必要 */
    pFileTime->dwLowDateTime = (DWORD) n64;
    pFileTime->dwHighDateTime = (DWORD) (n64 >> 32);
}

/* ファイルのディスク上のサイズを計算する関数 */
/* ※ この関数自体は Unicode を考慮していません */
DWORD GetFileSizeOnDisk(const char* pszFileName)
{
    struct _stat fileStatus;
#ifdef _DOS
    diskfree_t diskData;
#else
    _diskfree_t diskData;
#endif
    char chDrive;
    DWORD dwSizeOfCluster, dwClusters;

    if (_stat(pszFileName, &fileStatus) || !fileStatus.st_size)
        return 0;
    chDrive = tolower(*pszFileName);
#ifdef _DOS
    _dos_getdiskfree(chDrive - 'a' + 1, &diskData);
#else
    _getdiskfree(chDrive - 'a' + 1, &diskData);
#endif

    /* 1クラスタあたりのバイト数を出す */
    dwSizeOfCluster = (DWORD) diskData.bytes_per_sector * (DWORD) diskData.sectors_per_cluster;
    /* ファイルが使用しているクラスタ数を出す */
    if (fileStatus.st_size % dwSizeOfCluster == 0)
        dwClusters = (DWORD) fileStatus.st_size / dwSizeOfCluster;
    else
        dwClusters = (DWORD) fileStatus.st_size / dwSizeOfCluster + 1;
    return dwClusters * dwSizeOfCluster;
}