TL;DR
- 既存フォーマット(tar, zip, 7z)は優秀だが、メタデータを完全に省略できない
- PNA (Portable Network Archive) は「名前」と「中身」以外すべてオプショナル
- 再現可能ビルド、プライバシー保護、ストリーミング処理に強い
正直に言います
99%のユースケースでは tar や zip で十分です。
でも、残り1%で本当に困る場面がある。この記事は、その1%に該当する人のために書きました。
困った場面①:メタデータが漏れる
アーカイブを外部に公開するとき、意図せず情報が漏れることがあります。
$ tar tvf suspicious.tar
-rw-r--r-- john/developers 1234 2024-03-15 09:23 secret.txt
- ユーザー名
john - グループ名
developers - ファイル更新日時
「別に見られても困らない」?本当にそうでしょうか。
- 開発者の実名や社内グループ構造が推測される
- タイムスタンプから開発スケジュールが推測される
- ファイルシステムの種類や環境が推測される
セキュリティ監査で「不要な情報は含めるな」と言われても、既存フォーマットでは難しい。
困った場面②:再現可能ビルドが壊れる
CI/CDで「同じソースから同じ成果物」を作りたい。当たり前の要求に思えますが、アーカイブが絡むと途端に難しくなります。
# 開発マシンでアーカイブ(uid=501, user=alice)
$ tar cvf a.tar myfile.txt
# CIランナーでアーカイブ(uid=1001, user=runner)
$ tar cvf b.tar myfile.txt
# 同じファイルなのにハッシュが一致しない
$ sha256sum a.tar b.tar
3a7f8b2c... a.tar
9e8f7a6b... b.tar # uid/gid/usernameが異なるため不一致
tarはファイルの所有者情報やタイムスタンプを自動的に記録します。ビルド環境が変われば、ファイルの内容が同一でもアーカイブのハッシュは変わります。
--mtime や --owner で固定値を指定すれば回避できます:
$ tar --mtime='1970-01-01' --owner=0 --group=0 -cvf reproducible.tar myfile.txt
しかし:
- 毎回オプションを指定する必要がある(忘れると壊れる)
- メタデータフィールド自体は存在する(固定値が埋め込まれる)
- ツールやスクリプトごとに対応が必要
zipも同様で、タイムスタンプを固定するには外部ツールや後処理が必要です。
PNAという選択肢
Portable Network Archive (PNA) は、これらの問題を設計レベルで解決するために作られたアーカイブフォーマットです。CLIツールとして提供されており、Rust以外の環境でもシェルから直接利用できます。
設計思想:メタデータはオプショナル
PNAでは、エントリに必須なのは:
- ファイル名(エントリ識別子)
- ファイル本体(ペイロード)
それ以外はすべて省略可能。 タイムスタンプ、パーミッション、所有者情報、拡張属性——必要なければ記録しません。
# 最小構成でアーカイブ
$ pna create -f minimal.pna myfile.txt
# メタデータを保持してアーカイブ
$ pna create -f full.pna --keep-timestamp --keep-permission myfile.txt
他の特徴
| 機能 | PNA | tar | zip | 7z |
|---|---|---|---|---|
| ストリーミング書き込み | ✅ | ✅ | △※1 | ❌ |
| ストリーミング読み込み | ✅ | ✅ | ❌ | ❌ |
| per-fileの圧縮 | ✅ | ❌ | ✅ | ✅ |
| ソリッド圧縮 | ✅ | △※2 | ❌ | ✅ |
| 暗号化(256-bit AES) | ✅ | ❌ | △※3 | ✅ |
| メタデータ省略 | ✅ | △※4 | ❌ | ❌ |
| 分割アーカイブ | ✅ | ❌ | ✅ | ✅ |
※1 zip: central directoryが末尾にあるため完全なストリーミングは不可
※2 tar: .tar.gz等の外部圧縮で類似の効果は得られるが、エントリ間の辞書共有ではなくバイト列全体の圧縮であり、per-fileアクセスは失われる
※3 zip: AES暗号化は拡張仕様で実装依存
※4 tar: オプションで固定値にはできるが、フィールド自体は存在する
実際に使ってみる
インストール
# macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf \
'https://github.com/ChanTsune/Portable-Network-Archive/releases/latest/download/portable-network-archive-installer.sh' | sh
# または cargo
cargo install portable-network-archive
基本操作
# 作成
$ pna create -f archive.pna file1.txt file2.txt dir/
# 展開
$ pna extract -f archive.pna
# 一覧
$ pna list -f archive.pna
再現可能なアーカイブを作る
# メタデータを含めない(デフォルト)
$ pna create -f reproducible.pna src/
# 何度実行しても同じハッシュ
$ sha256sum reproducible.pna
a1b2c3d4... reproducible.pna
なぜ再現可能になるのか
tarやzipでは、メタデータフィールドが仕様上必須です。タイムスタンプやオーナー情報は「記録しない」という選択ができず、固定値で埋める必要があります。固定値自体はバイト列として存在するため、ツールやバージョンによって埋め方が異なるリスクが残ります。
PNAではオプショナルなチャンクを物理的に省略します。アーカイブに残るのはファイル名とデータ本体だけなので、同じ入力からは常に同じバイト列が生成されます。
具体的には、以下の条件が揃えば同一ハッシュが保証されます:
- 入力ファイルの内容とファイル名が同一
- 圧縮アルゴリズムとそのパラメータが同一
- エントリの追加順序が同一
暗号化 + 圧縮
# AES-256 + zstd圧縮
$ pna create -f secure.pna \
--compression zstd \
--encryption aes \
--password \
sensitive-data/
ストリーミング処理
# 標準入出力でパイプ処理
$ tar cf - src/ | pna create -f - --stdin | curl -X PUT ...
# ネットワーク経由で直接展開
$ curl -s https://example.com/data.pna | pna extract -f -
いつPNAを使うべきか
向いているケース
-
再現可能ビルドが必要
- CI/CDでキャッシュキーとしてアーカイブのハッシュを使いたい
- 「同じ入力→同じ出力」を保証したい
-
プライバシーが重要
- 公開するアーカイブに環境情報を含めたくない
- セキュリティ監査で情報漏洩を指摘された
-
ストリーミング + 暗号化
- ネットワーク経由でリアルタイム処理したい
- かつ、暗号化も必要
-
per-file圧縮 + ソリッド圧縮の選択
- 通常はper-fileで個別アクセス可能に
- 配布用はソリッドで最大圧縮
向いていないケース
- とにかく互換性重視 → zip/tarを使う
- 既存ツールチェーンに組み込みたい → zip/tarを使う
- チームメンバーが新しいツールを覚えたくない → zip/tarを使う
正直に言います。普及度では圧倒的に負けています。 そこは認めた上で、「この場面ではPNAが最適解」というニッチを狙っています。
技術的な背景
PNAはPNG画像フォーマットのチャンク構造に着想を得ています。
PNGから継承した設計原則
PNGのチャンク構造は3つの利点を持ちます:
- 未知チャンクのスキップ — 読み取り側が知らないチャンクは長さを見て飛ばせる
- チャンク単位の整合性検証 — 各チャンクにCRCが付き、破損を局所的に検出できる
- 拡張性 — 新しいチャンクタイプを追加しても既存ツールが壊れない
PNAはこの3原則をアーカイブに転用しました。
チャンクタイプの命名規則
PNGでは、チャンクタイプの4文字の大文字/小文字にそれぞれ意味があります。PNAもこの規則をそのまま継承しています。
| 文字位置 | 大文字 | 小文字 |
|---|---|---|
| 1文字目 | Critical(必須、理解できなければエラー) | Ancillary(補助的、無視しても安全) |
| 2文字目 | Public(標準仕様) | Private(アプリ独自) |
| 3文字目 | 予約(大文字固定) | — |
| 4文字目 | Unsafe to copy(不明なら複製禁止) | Safe to copy(不明でも複製可) |
たとえば FHED(ファイルヘッダ)は全大文字なので Critical/Public。一方 cTIM(作成日時)は小文字始まりなので Ancillary — つまりチャンク名そのものが「これは省略してよい」という情報を持っているのです。
この設計により、将来新しいメタデータチャンクが追加されても、古いツールは未知の ancillary チャンクを安全にスキップできます。
チャンクの物理レイアウト
各チャンクは以下のバイト列で構成されます:
| オフセット | サイズ | 内容 |
|---|---|---|
| 0 | 4 bytes | データ長(ビッグエンディアン) |
| 4 | 4 bytes | チャンクタイプ(ASCII 4文字) |
| 8 | N bytes | チャンクデータ |
| 8+N | 4 bytes | CRC-32 |
アーカイブの全体構造
┌──────────────────────────────────────┐
│ Archive Header │
├──────────────────────────────────────┤
│ Entry 1 │
│ ├─ File Header Chunk (FHED) │
│ ├─ [Ancillary] Timestamp (cTIM) │
│ ├─ [Ancillary] Permission (fPRM) │
│ ├─ File Data Chunk (FDAT) │
│ └─ File End Chunk (FEND) │
├──────────────────────────────────────┤
│ Entry 2 │
│ └─ ... │
├──────────────────────────────────────┤
│ Archive End (AEND) │
└──────────────────────────────────────┘
Ancillaryチャンク(タイムスタンプ、パーミッション等)は「チャンクが存在しない」という形で物理的に省略されます。既存フォーマットのように「ダミー値を入れる」必要がないため、ファイルシステム由来のメタデータを一切含まないアーカイブが可能です。
PNGから変えた点
- エントリの導入 — PNGは単一画像だが、PNAは複数ファイルを扱うためエントリ概念を追加
- 圧縮・暗号化の柔軟性 — PNGはフィルタ+DEFLATE固定だが、PNAはチャンクデータ層でzstd/AES等を選択可能
- ソリッドモード — 複数エントリを1つの圧縮ストリームにまとめる選択肢を追加
ライブラリとしての利用
Rustライブラリ libpna も提供しています。
use libpna::{Archive, EntryBuilder, WriteOptions};
use std::fs::File;
use std::io::Write;
fn main() -> std::io::Result<()> {
let file = File::create("example.pna")?;
let mut archive = Archive::write_header(file)?;
// メタデータなしでエントリを追加
let mut entry = EntryBuilder::new_file(
"hello.txt".into(),
WriteOptions::builder().build(),
)?;
entry.write_all(b"Hello, PNA!")?;
archive.add_entry(entry.build()?)?;
archive.finalize()?;
Ok(())
}
まとめ
PNAは「tar/zipを置き換える」ことを目指していません。
「tar/zipでは解決しにくい問題を、設計レベルで解決する」 ことを目指しています。
- 再現可能ビルドが必要なとき
- メタデータ漏洩を防ぎたいとき
- ストリーミング + 暗号化が必要なとき
もしこれらに該当するなら、PNAを試してみてください。