Ripara ora
- Trascina il file
- La riparazione avviene in locale
- Scarica il risultato
Il tipo di persistent store predefinito e consigliato di Core Data è NSSQLiteStoreType: un normale database SQLite 3 su disco, di solito chiamato <AppName>.sqlite dentro la directory Library dell'app. Quando non si apre, il coordinatore dello store lancia un errore NSCocoaErrorDomain e SQLite definisce il file malformato, ma un database malformato è raramente vuoto: le righe sono ancora nelle loro pagine, dietro un indice rotto. Trascina il .sqlite qui sopra — e aggiungi il suo file collaterale -wal se ce l'hai — e lo strumento legge il file nel tuo browser, ricostruisce lo schema e reinserisce ogni riga che riesce ancora a decodificare in uno store nuovo e apribile. Non si installa nulla e il database non viene mai caricato.
Cosa scrive davvero Core Data sul disco
Core Data è un framework di grafo di oggetti e persistenza, non un database — il database è SQLite, e Core Data ne controlla la disposizione. Apri uno store Core Data in un qualsiasi browser SQLite e non vedrai nomi di tabella comprensibili; vedrai uno schema deformato generato dal framework. Ogni entità diventa una tabella chiamata Z + il nome dell'entità in maiuscolo, quindi un'entità Person è la tabella ZPERSON. Ogni riga porta tre colonne di sistema: Z_PK (la chiave primaria intera), Z_ENT (quale entità/sottoclasse è la riga) e Z_OPT (un contatore di versione per il blocco ottimistico). Anche i tuoi attributi hanno un prefisso — firstName diventa ZFIRSTNAME — e una relazione a-uno è memorizzata come colonna di chiave esterna che contiene il Z_PK della riga di destinazione.
Accanto ai tuoi dati ci sono tre tabelle di servizio. Z_PRIMARYKEY contiene una riga per entità con le colonne Z_ENT, Z_NAME, Z_SUPER e Z_MAX — la chiave primaria più alta assegnata finora, che Core Data incrementa per allocare l'inserimento successivo. Z_METADATA contiene Z_VERSION, Z_UUID e Z_PLIST, un blob plist binario che memorizza i metadati dello store, inclusi NSStoreModelVersionHashes e l'UUID dello store. Z_MODELCACHE mette in cache il modello compilato. Le date sono memorizzate come un valore REAL di secondi a partire dalla data di riferimento di Cocoa, 2001-01-01 00:00:00 UTC — non l'epoca Unix — quindi un timestamp grezzo pari a 0 è il primo gennaio 2001.
Tutto questo vive in un contenitore SQLite standard: un array piatto di pagine di dimensione fissa (4096 byte per impostazione predefinita), dove i primi 16 byte del file sono la stringa magica SQLite format 3\000. La pagina 1 contiene l'intestazione del file e lo schema sqlite_master; ogni tabella Z è un b-tree le cui pagine interne puntano verso il basso alle pagine foglia che contengono i record veri e propri. Rompi un puntatore e SQLite rifiuta l'intero file — anche se le pagine foglia piene di righe sono intatte.
I file -wal e -shm: quale contiene i tuoi dati
Da iOS 7 (e OS X 10.9 Mavericks) Core Data apre il suo store in modalità WAL — write-ahead logging — per impostazione predefinita. Ecco perché un singolo store è in realtà fino a tre file su disco: <AppName>.sqlite, <AppName>.sqlite-wal e <AppName>.sqlite-shm. Capire la differenza decide se i tuoi dati più recenti sopravvivono.
Il file -wal è il log di scrittura anticipata (write-ahead log). Quando la tua app conferma (commit) una transazione, le pagine modificate vengono aggiunte in coda al -wal come frame invece di essere scritte direttamente nel .sqlite principale; solo un checkpoint periodico le reintegra nel database. La conseguenza: se l'app è andata in crash o è stata chiusa forzatamente prima di un checkpoint, le righe confermate più recenti possono vivere solo nel -wal. La sua disposizione è precisa — un'intestazione di 32 byte (numero magico 0x377f0682 o 0x377f0683, una versione di formato, la dimensione di pagina, una sequenza di checkpoint, due valori di salt e un checksum) seguita da frame, ognuno con un'intestazione di frame di 24 byte (numero di pagina, la dimensione del database in pagine per un frame di commit o 0 altrimenti, i due salt e due checksum) più una pagina di dati.
Il file -shm è l'indice-wal in memoria condivisa (wal-index): una struttura di lookup che SQLite usa per trovare rapidamente le pagine dentro il -wal. È interamente dato derivato — non contiene nessuna delle tue righe e SQLite lo ricostruisce automaticamente a partire dal -wal. Quindi la regola è semplice: porta il -wal, ignora lo -shm. Trascina prima il .sqlite, poi aggiungi il -wal nello slot collaterale opzionale: i suoi frame confermati vengono sovrapposti prima della scansione, così il recupero riflette l'ultima transazione confermata invece dell'ultimo checkpoint. Copiare solo il .sqlite da un dispositivo e lasciare indietro il -wal è il modo classico di perdere dati recenti in silenzio — una trappola sia per il recupero dopo un crash sia per l'estrazione forense.
Gli errori che vedi e cosa recupera il salvataggio delle righe
A livello di SQLite lo store fallisce con uno di due codici di risultato. SQLITE_CORRUPT (codice 11) stampa "database disk image is malformed": il motore ha seguito un puntatore e ha trovato qualcosa che non è una pagina b-tree valida — una pagina azzerata, un offset di cella errato, un conteggio di pagine che non concorda con la lunghezza del file. SQLITE_NOTADB (codice 26) stampa "file is not a database": i 16 byte magici dell'intestazione sono errati, il che significa o che i byte dell'intestazione sono stati danneggiati (le pagine dietro di essi spesso sono a posto) oppure che il file è davvero qualcos'altro o è cifrato. Core Data lo incapsula e presenta NSCocoaErrorDomain codice 259 (NSFileReadCorruptFileError) — "The file couldn't be opened because it isn't in the correct format." — mentre il codice SQLite grezzo è riposto nello userInfo dell'errore sotto la chiave NSSQLiteErrorDomain, quindi una riga di log che riporta NSSQLiteErrorDomain=11 è la conferma che si tratta di corruzione a livello di pagina, non di un'incompatibilità di versione dello schema.
Il recupero non cerca di riparare il file rotto sul posto. Legge tutto ciò che è ancora decodificabile e lo scrive in un database del tutto nuovo — la stessa strategia del comando .recover di SQLite. Percorre il b-tree di ogni tabella Z e decodifica i record delle foglie; per le pagine che la catena di puntatori non riesce più a raggiungere, ricorre a una scansione grezza delle pagine, catturando record autodescrittivi direttamente dal file. Le righe che non si possono attribuire a una tabella conosciuta finiscono in una tabella lost_and_found invece di essere buttate via, e i valori Z_MAX di Z_PRIMARYKEY vengono ricostruiti a partire dalle righe recuperate, così l'app può continuare a inserire senza collisioni di chiave primaria. Poiché la scansione legge anche le pagine della freelist e non compattate (un-vacuumed), le righe che la tua app aveva cancellato prima possono riaffiorare — utile, ma è bene saperlo.
I dati binari esterni vivono fuori dallo store
Un limite onesto è specifico di Core Data. Quando un attributo binario (una foto, un PDF, dell'audio) ha spuntata l'opzione "Allows External Storage", Core Data decide valore per valore se incorporare i byte nella riga oppure scriverli come file separato quando superano all'incirca i 100 KB. Questi blob esterni vengono memorizzati in una directory di supporto nascosta accanto allo store — .<storename>_SUPPORT/_EXTERNAL_DATA/ — con nomi di file UUID, e la riga del database conserva solo un riferimento a quel file, non i byte.
Quindi recuperare il solo .sqlite riporta indietro ogni attributo scalare e ogni blob incorporato, ma per i dati memorizzati esternamente restituisce il riferimento, non l'immagine. Se hai anche la cartella _EXTERNAL_DATA dello stesso container dell'app, quei file si riabbinano per nome; se quella directory è sparita, nessun recupero del database può ricostruire byte che non sono mai stati dentro al database. E poiché uno store Core Data può contenere l'intero stato privato di un'app — ogni nota, messaggio, dato sanitario o account che un utente abbia mai salvato — tutto questo gira nel tuo browser: il file viene letto dal disco, ricostruito nella scheda, e puoi guardare la scheda Rete confermare che 0 byte lasciano la tua macchina.
Cosa può e cosa non può riparare
Può riparare
- "database disk image is malformed" (SQLITE_CORRUPT / codice 11) causato da una pagina b-tree danneggiata o da un puntatore di cella errato, con le pagine ZENTITY retrostanti intatte
- Righe di ogni tabella ZENTITY che si decodificano ancora — estratte e reinserite in uno store .sqlite nuovo e apribile
- Modifiche confermate senza checkpoint, sovrapposte da un file collaterale -wal fornito (le righe più recenti dopo un crash o una chiusura forzata)
- Uno store la cui intestazione di 16 byte o il cui schema è danneggiato ma le cui pagine foglia sopravvivono — lo schema viene ricostruito a partire dalle pagine
- Righe che i puntatori del b-tree non raggiungono più, recuperate con una scansione grezza delle pagine in una tabella lost_and_found, con Z_PRIMARYKEY.Z_MAX ricostruito
Non può riparare
- Righe sovrascritte fisicamente o troncate dalla fine del file — quei byte non esistono più sul disco
- Blob binari esterni memorizzati in .<storename>_SUPPORT/_EXTERNAL_DATA quando non hai anche quei file — la riga contiene solo un riferimento
- Store cifrati (dati con NSFileProtectionComplete copiati mentre il dispositivo era bloccato, oppure SQLCipher) senza la chiave — le pagine si leggono come testo cifrato
- Il file -shm da solo: è un indice-wal ricostruibile, non i tuoi dati — il -wal è il file che porta i commit recenti
- Una garanzia che i dati recuperati siano completi o corretti — i dati provenienti da uno store danneggiato sono sempre da mettere in dubbio; verificali confrontandoli con una fonte attendibile
Se una riparazione fallisce, ti diciamo perché (dati mancanti rispetto a struttura danneggiata), e non ti viene mai addebitato nulla per una riparazione fallita.