メンテナンス版、2026-09-01確認。 2019年の原文は空ではなく、Windowsの
compactを再帰実行するコマンドが1つ掲載されていました。その全文を末尾に保存しています。メンテナンス版では、タイトルから連想されるもう1つの意味、つまりドライブ内のZIP/TARアーカイブを探す作業を扱いますが、ドライブ全体の無差別な展開は勧めません。
Table of Contents
最初に「展開」の意味を区別する
互いに無関係な2つの操作が、どちらも展開や解凍と呼ばれます。
- NTFSファイルシステム圧縮: Windowsが通常のファイルを圧縮して保存し、アプリケーションは普段どおり開きます。Microsoftの`compact`ドキュメントでは、
/uがこの属性を解除し、/sがディレクトリ以下を再帰処理すると説明されています。 - アーカイブ展開: ZIPやTAR系コンテナー内の名前付きメンバーを、宛先ディレクトリへ書き出します。Pythonは`zipfile`と`tarfile`でこれらを扱います。
compactはZIPを開かず、ZIPの展開はNTFS圧縮属性を変えません。アーカイブに残したコマンドは、現在のドライブ上のファイルシステム保存方式を再帰的に変更するものです。タイトルが「すべてのアーカイブを解凍」に見えるという理由だけで実行しないでください。NTFS圧縮を扱う場合も、まず確認済みの1ディレクトリで状態を照会して試します。ボリューム全体の変更は大量の空き容量を消費し、保護されたOSファイルにも遭遇します。
ドライブ全体の無差別な展開が危険な理由
アーカイブは無害なファイルの袋ではありません。展開前に、少なくとも次を検討します。
- 絶対パス、
..要素、Windowsドライブパス、移植できない名前 - シンボリックリンク、ハードリンク、デバイス、FIFOなどの特殊TARメンバー
- 重複名、ファイルとディレクトリの競合、大文字小文字だけが異なる衝突
- 巨大な出力や極端な展開比率を宣言するアーカイブ
- 無人処理をパスワード管理問題へ変える暗号化メンバー
- 2つのアーカイブが同じ宛先名へ書き込むこと
- 一部のファイルを作成した後に発生する失敗
Python公式ドキュメントは、展開前にアーカイブを検査するよう明示的に警告しています。TARのdata抽出フィルターでも、すべてのサービス拒否や悪意ある入力を防げません。安全な処理では、先に棚卸しし、新しい隔離ディレクトリへ展開し、資源上限を適用し、受け入れる前に結果を検証します。
保守的なワークフロー
- ソースドライブを変更せず、出力とログをスキャン対象ツリーの外へ置きます。
- まずアーカイブらしいファイル名を探し、拡張子だけを信じず内容からZIP/TAR形式を確認します。
- SHA-256、メンバー数、宣言された非圧縮サイズ、展開比率、すべての拒否理由を記録します。
- JSON Linesマニフェストを確認します。
problemsがある記録は展開しません。 - 毎回、相対パスを1つだけ選んで展開します。
- 既存リンクも既存ファイルもない、新しい一時ディレクトリへ展開します。
- すべての通常ファイルのパスとサイズを厳密に検証し、状態記録を書いてから、完成したディレクトリを最終位置へ改名します。
- 元のアーカイブは残します。削除は、バックアップとアプリケーションレベルの確認後に別途判断する保持方針です。
これは意図的に再帰ワンライナーより遅い処理です。その代わり、部分的失敗、衝突、再試行を観測できます。
Python 3.12+の監査・展開リファレンスツール
次の標準ライブラリだけのスクリプトは、ZIPと読み取り可能なTAR形式を扱います。既定では棚卸しだけを行い、--extractは棚卸し内の1つのアーカイブパスだけを受け付けます。リンクと特殊ファイル、パストラバーサルとWindowsドライブパス、重複名と大文字小文字の衝突、暗号化ZIPメンバー、上限超過のメンバー数・宣言バイト数・展開比率を拒否します。
上限値はポリシー例であり、普遍的な安全値ではありません。信頼できないソースには、さらに低い値を設定してください。低権限アカウントと適切に制限された環境で不信な内容を扱います。このスクリプトはガードレールであり、マルウェアスキャナーやセキュリティ境界ではありません。
<details>
<summary><code>archive_audit.py</code>を表示</summary>
#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import re
import shutil
import stat
import tarfile
import tempfile
import zipfile
from pathlib import Path, PurePosixPath, PureWindowsPath
SUFFIXES = (
".zip", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2",
".tar.xz", ".txz", ".gz", ".bz2", ".xz",
)
WINDOWS_RESERVED = {
"con", "prn", "aux", "nul",
*(f"com{i}" for i in range(1, 10)),
*(f"lpt{i}" for i in range(1, 10)),
}
def digest(path):
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def normalized_name(raw):
if not raw or "