壊れた iOS の Core Data ストアを復元する

Swift と Objective-C の API の下で、Core Data はごく普通の SQLite 3 データベースを書き込みます。アプリがストアは「正しい形式ではない(isn't in the correct format)」と報告するとき、たいていは1つのページかポインタが壊れただけで — アプリが保存した行はまだファイルの中に残っています。それを取り戻すとは、ファイルをページ単位で読むことであって、ユーザーのデータをサーバーに引き渡すことではありません。

ファイルがデバイスから出ることはありません — 修復はブラウザ内で実行されます。アップロードは 0 バイトです。

Your data is the big block — usually intact. What breaks is the small index. Repair rebuilds it.

Drop a file here or browseTap to choose a file

ZIP · Office · PDF · video · JPG · PNG · RAR/7z · SQLite — repaired right here in your browser. Nothing is uploaded

0 bytes uploadedFree: 3 repairs/day — up to 500 MB video, 100 MB docs & archives, 50 MB photos. Free download. Single repair €5.90. No account required.

今すぐ修復

  1. ファイルをドロップ
  2. 修復はローカルで実行
  3. 結果をダウンロード

Core Data の既定かつ推奨される永続ストアタイプは NSSQLiteStoreType — ディスク上のごく普通の SQLite 3 データベースで、通常はアプリの Library ディレクトリ内に <AppName>.sqlite という名前で置かれます。開けなくなると、ストアコーディネータは NSCocoaErrorDomain エラーを投げ、SQLite はそのファイルを malformed(不正)と判定しますが、不正なデータベースが空であることはめったにありません。行はまだ、壊れたインデックスの奥にあるそれぞれのページに残っています。上に .sqlite をドロップし — 手元にあればその -wal サイドカーも追加してください — すると、このツールはブラウザ内でファイルを読み、スキーマを再構築し、まだデコードできる行をすべて、新しく開けるストアに挿入し直します。何もインストールされず、データベースがアップロードされることは決してありません。

Core Data が実際にディスクへ書き込むもの

Core Data はオブジェクトグラフと永続化のフレームワークであって、データベースそのものではありません — データベースは SQLite で、そのレイアウトを支配しているのが Core Data です。Core Data ストアを任意の SQLite ブラウザで開いても、親しみやすいテーブル名は目に入りません。フレームワークが生成する変形されたスキーマが見えます。各エンティティは Z + エンティティ名を大文字にしたテーブルになるので、Person エンティティはテーブル ZPERSON です。各行は3つのシステム列を持ちます: Z_PK(整数の主キー)、Z_ENT(その行がどのエンティティ/サブクラスか)、そして Z_OPT(楽観的ロック用のバージョンカウンタ)です。属性にも接頭辞が付き — firstNameZFIRSTNAME になります — 対1のリレーションシップは、参照先の行の Z_PK を保持する外部キー列として格納されます。

データの隣には3つの管理用テーブルがあります。Z_PRIMARYKEY はエンティティごとに1行を持ち、列は Z_ENTZ_NAMEZ_SUPERZ_MAX — これまでに発行された最大の主キーで、Core Data は次の挿入を割り当てるためにこれをインクリメントします。Z_METADATAZ_VERSIONZ_UUIDZ_PLIST を保持し、Z_PLISTNSStoreModelVersionHashes やストアの UUID を含むストアメタデータを格納したバイナリ plist の blob です。Z_MODELCACHE はコンパイル済みモデルをキャッシュします。日付は Cocoa の基準日、2001-01-01 00:00:00 UTC からの秒数を表す REAL 値として格納されます — Unix エポックではありません — なので、生のタイムスタンプ 0 は 2001年の元日です。

これらはすべて標準的な SQLite コンテナの中に存在します: 固定サイズのページ(既定で 4096 バイト)が並んだ平坦な配列で、ファイルの先頭 16 バイトはマジック文字列 SQLite format 3\000 です。ページ 1 はファイルヘッダと sqlite_master スキーマを保持し、各 Z テーブルはb-treeで、その内部ページは実際のレコードを保持するリーフページへと下向きに指しています。ポインタが1つ壊れると SQLite はファイル全体を拒否します — たとえ行で満たされたリーフページが無傷であっても。

-wal と -shm ファイル: あなたのデータを持っているのはどちら

iOS 7(および OS X 10.9 Mavericks)以降、Core Data は既定でストアを WAL モード — write-ahead logging(先行書き込みログ)— で開きます。だからこそ、1つのストアは実際にはディスク上で最大3つのファイルになります: <AppName>.sqlite<AppName>.sqlite-wal、そして <AppName>.sqlite-shm です。この違いを理解することが、あなたの最新のデータが生き残るかどうかを決めます。

-wal ファイルは先行書き込みログです。アプリがトランザクションをコミットすると、変更されたページはメインの .sqlite に直接書き込まれるのではなく、-walフレームとして追記されます。それらをデータベースに畳み込み直すのは、定期的なチェックポイントだけです。その帰結として、チェックポイントの前にアプリがクラッシュしたり強制終了されたりした場合、最も新しくコミットされた行は -wal の中にだけ存在しうるのです。そのレイアウトは厳密です — 32 バイトのヘッダ(マジック 0x377f0682 または 0x377f0683、フォーマットバージョン、ページサイズ、チェックポイントシーケンス、2つの salt 値、そしてチェックサム)に続いてフレームが並び、各フレームは 24 バイトのフレームヘッダ(ページ番号、コミットフレームの場合はページ数で表したデータベースサイズ・それ以外は 0、2つの salt、2つのチェックサム)と1ページ分のデータからなります。

-shm ファイルは共有メモリの wal-indexです: SQLite が -wal 内のページを素早く見つけるために使う検索構造です。これは完全に派生データで — あなたの行を1つも保持しておらず、SQLite が -wal から自動的に再構築します。だからルールは単純です: -wal は持ち込み、-shm は無視する。まず .sqlite をドロップし、次にオプションのサイドカー枠に -wal を追加すると、そのコミット済みフレームがスキャン前に重ね合わされ、復元は最後のチェックポイントではなく最後にコミットされたトランザクションを反映します。デバイスから .sqlite だけをコピーして -wal を置き去りにするのは、最近のデータを静かに失う典型的な手口です — クラッシュからの復旧にとっても、フォレンジック抽出にとっても罠です。

表示されるエラーと、行の救出で戻ってくるもの

SQLite の層では、ストアは2つの結果コードのいずれかで失敗します。SQLITE_CORRUPT(コード 11)は 「database disk image is malformed」(データベースのディスクイメージが不正)と出力します: エンジンがポインタをたどった先に、有効な b-tree ページではないもの — ゼロ埋めされたページ、不正なセルオフセット、ファイル長と食い違うページ数 — を見つけたということです。SQLITE_NOTADB(コード 26)は 「file is not a database」(ファイルはデータベースではない)と出力します: 16 バイトのヘッダマジックが誤っているという意味で、ヘッダのバイトが壊れた(その奥のページは無事なことが多い)か、あるいはファイルが本当に別物か暗号化されているかのどちらかです。Core Data はこれを包んで NSCocoaErrorDomain コード 259NSFileReadCorruptFileError)— 「The file couldn't be opened because it isn't in the correct format.」(ファイルは正しい形式ではないため開けませんでした)— として表に出す一方、生の SQLite コードはエラーの userInfo 内、NSSQLiteErrorDomain キーの下に収められます。したがって、NSSQLiteErrorDomain=11 と読めるログ行こそ、これがスキーマのバージョン不一致ではなくページレベルの破損だという確証です。

復元は、壊れたファイルをその場で直そうとはしません。まだデコードできるものをすべて読み出し、まっさらな新しいデータベースに書き込みます — SQLite 自身の .recover コマンドと同じ戦略です。各 Z テーブルの b-tree を歩いてリーフレコードをデコードし、ポインタの連鎖がもう到達できないページについては生のページ走査にフォールバックして、自己記述的なレコードをファイルから直接拾い上げます。既知のテーブルに帰属させられない行は捨てられずに lost_and_found テーブルに収まり、Z_PRIMARYKEYZ_MAX 値は復元された行から再構築されるので、アプリは主キーの衝突なしに挿入を続けられます。走査は freelist や vacuum 前のページも読むため、アプリが以前に削除した行が再浮上することもあります — 便利ですが、知っておく価値があります。

外部バイナリデータはストアの外に存在する

1つの正直な限界は Core Data 特有のものです。バイナリ属性(写真、PDF、音声)に 「Allows External Storage」(外部ストレージを許可)がチェックされていると、Core Data は値ごとに、バイトを行にインライン化するか、約 100 KB を超えたら別ファイルとして書き出すかを判断します。それらの外部 blob は、ストアの隣にある隠しサポートディレクトリ — .<storename>_SUPPORT/_EXTERNAL_DATA/ — の下に UUID のファイル名で保存され、データベースの行はそのファイルへの参照だけを保持し、バイトそのものは保持しません。

したがって .sqlite だけを復元すると、すべてのスカラー属性とインライン化されたすべての blob は戻ってきますが、外部に保存されたデータについては画像ではなく参照が返ります。同じアプリコンテナからの _EXTERNAL_DATA フォルダも手元にあれば、それらのファイルは名前で再びペアになります。そのディレクトリが失われていれば、そもそもデータベースの中に無かったバイトを、どんなデータベース復元も再構築することはできません。そして Core Data ストアはアプリのプライベートな状態すべて — ユーザーがこれまで保存したあらゆるメモ、メッセージ、健康記録、アカウント — を保持しうるため、これらはすべてブラウザ内で実行されます: ファイルはディスクから読まれ、タブ内で再構築され、Network タブであなたのマシンから 0 バイトも出ていないことを確認できます。

修復できるものとできないもの

修復できる

  • 「database disk image is malformed」(SQLITE_CORRUPT / コード 11)— b-tree ページの破損や不正なセルポインタが原因で、その背後にある ZENTITY ページが無傷なもの
  • 各 ZENTITY テーブルで、まだデコードできる行 — 取り出して、新しく開ける .sqlite ストアに挿入し直します
  • 供給された -wal サイドカーから重ね合わされた、チェックポイント未適用のコミット済み変更(クラッシュや強制終了の後の最も新しい行)
  • 16 バイトのヘッダまたはスキーマが壊れていても、リーフページが生き残っているストア — スキーマはページから再構築されます
  • b-tree のポインタがもう到達できない行 — 生のページ走査で lost_and_found テーブルへ復元し、Z_PRIMARYKEY.Z_MAX を再構築します

修復できない

  • 物理的に上書きされた、あるいはファイルの末尾から切り詰められた行 — それらのバイトはもうディスク上に存在しません
  • .<storename>_SUPPORT/_EXTERNAL_DATA に保存された外部バイナリ blob で、そのファイルも手元に無い場合 — 行は参照だけを保持しています
  • 暗号化されたストア(デバイスがロックされたまま NSFileProtectionComplete のデータをコピーしたもの、または SQLCipher)で鍵が無い場合 — ページは暗号文として読まれます
  • -shm ファイル単体: これは再構築可能な wal-index であってあなたのデータではありません — 最近のコミットを運ぶのは -wal です
  • 復元されたデータが完全である、あるいは正しいという保証 — 壊れたストアから出たデータは常に疑わしいものです。既知の信頼できる情報源と照合して検証してください

修復が失敗した場合、その理由(データの欠落か構造の破損か)をお伝えします。失敗した修復に料金が請求されることはありません。

よくある質問

アプリが「The file couldn't be opened because it isn't in the correct format.」と記録します。データは消えたのですか?

たいていは違います。それは NSCocoaErrorDomain コード 259(NSFileReadCorruptFileError)で、userInfo 内の NSSQLiteErrorDomain の下にある生の SQLite コードはほぼ必ず 11 — SQLITE_CORRUPT、「database disk image is malformed」です。これは1つのページかポインタが壊れて SQLite がファイル全体を拒否したという意味で、あなたの行を保持するリーフページはたいてい無傷で、新しいストアへ読み出せます。

-wal と -shm ファイルが必要ですか、それとも .sqlite だけでいいですか?

.sqlite を持ち込み、手元にあれば -wal も持ち込んでください。クラッシュの後、最も新しくコミットされた行はチェックポイントされるまで -wal の中にだけ存在しうるので、それをサイドカーとして追加すると最後のトランザクションが復元されます。-shm は再構築可能な共有メモリインデックスで、あなたのデータを1つも保持していません — 無視してかまいません。.sqlite だけをコピーして -wal を置き去りにするのは、最近のデータを失う最もよくある手口です。

なぜテーブルは ZPERSON、Z_PK、Z_METADATA などと呼ばれているのですか?

それは Core Data 自身の変形されたスキーマです。各エンティティは Z+ENTITY テーブルになり、各行はシステム列 Z_PKZ_ENTZ_OPT を持ち、属性には Z の接頭辞が付き、管理情報は Z_PRIMARYKEYZ_METADATAZ_MODELCACHE に存在します。復元はこれらの名前を保ち、Z_PRIMARYKEYZ_MAX カウンタを再構築するので、復元されたストアはあなたのアプリで再び開き、新しい行も挿入できます。

写真や添付ファイルは戻ってきますか?

データベースの中に保存されていた場合だけです。「Allows External Storage」が有効なバイナリ属性は、約 100 KB を超える値を隠しディレクトリ .<storename>_SUPPORT/_EXTERNAL_DATA の下の別ファイルに書き出し、行には参照だけを残します。.sqlite を復元するとその参照が返ります。バイトが戻るのは、同じアプリコンテナからの _EXTERNAL_DATA フォルダも手元にある場合だけです。

アプリのデータベースをオンラインの修復ツールに通しても安全ですか?

Core Data ストアは、あなたが管理しないサーバーに渡すには最悪の部類のファイルです — アプリがユーザーについて保存したあらゆる記録、メッセージ、認証情報を含みうるからです。ここでは何もアップロードされません: ファイルはあなたのディスクから読まれ、あなたのブラウザ内で再構築され、Network タブを開いてデータベースが1バイトも送信されていないことを確認できます。

関連: SQLite データベースを復元する · Excel ファイルを修復する · アップロードゼロの主張を検証する