Skip to content
Back to skills

Mysword Sqlite Forensics

ASecurity

Especialista em engenharia reversa de arquivos SQLite desconhecidos do ecossistema MySword/MyBible — quando o schema não está documentado ou o arquivo tem estrutura inesperada. Cobre: técnicas de inspeção de tabelas e índices (sqlite_master), identificação de tipo por assinatura de tabelas, leitura segura de campos desconhecidos, parsing defensivo de dados sem schema garantido, detecção de WAL mode e recovery, diagnóstico de arquivos corrompidos, análise dos arquivos mydata/ (bookmarks.mybibl...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 5, 2026
databasesgokotlinbashsqldatabase

Works with

  • cursor

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 5, 2026

npx -y skills add welitonevoc/Biblia-Codex --skill mysword-sqlite-forensics --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mysword Sqlite Forensics?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mysword Sqlite Forensics
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/welitonevoc-mysword-sqlite-forensics/badge)](https://www.skillsdirectory.com/skills/welitonevoc-mysword-sqlite-forensics)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mysword-sqlite-forensics
description: Especialista em engenharia reversa de arquivos SQLite desconhecidos do ecossistema MySword/MyBible — quando o schema não está documentado ou o arquivo tem estrutura inesperada. Cobre: técnicas de inspeção de tabelas e índices (sqlite_master), identificação de tipo por assinatura de tabelas, leitura segura de campos desconhecidos, parsing defensivo de dados sem schema garantido, detecção de WAL mode e recovery, diagnóstico de arquivos corrompidos, análise dos arquivos mydata/ (bookmarks.mybible, highlight.mybible, format.mybible, settings.mybible, tags.mybible, verselist.mybible, default.xrefs.twm), e estratégias de fallback para módulos de terceiros com schema não-padrão. Use quando o schema é desconhecido, quando há crash ao abrir um módulo, ou quando dados inesperados aparecem. Triggers: schema desconhecido, unknown schema, arquivo corrompido, crash ao abrir módulo, tabela não encontrada, no such table, colunas inesperadas, mydata forensics, bookmark schema, highlight schema, settings schema, format.mybible opaque, xrefs.twm, WAL recovery, sqlite_master inspect.
allowed-tools: Read, Glob, Grep, Bash
---

# MySword SQLite Forensics — Engenharia Reversa de Arquivos

> **Use quando:** O schema do arquivo é desconhecido, incompleto, ou diferente do esperado.  
> **Princípio:** Nunca assumir. Sempre inspecionar `sqlite_master` primeiro.

---

## 🔬 Passo 1 — Inspeção Completa de Qualquer Arquivo SQLite

```kotlin
/**
 * Ponto de entrada para qualquer arquivo .mybible ou .SQLite3 desconhecido.
 * Executa ANTES de qualquer query de dados.
 */
fun inspectSQLiteFile(filePath: String): SQLiteInspectionResult {
    return SQLiteDatabase.openDatabase(filePath, null, SQLiteDatabase.OPEN_READONLY).use { db ->

        // 1. Listar TODAS as tabelas
        val tables = db.rawQuery(
            "SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name", null
        ).use { c -> buildList { while (c.moveToNext()) add(c.getString(0) to c.getString(1)) } }

        // 2. Listar TODOS os índices
        val indexes = db.rawQuery(
            "SELECT name, tbl_name, sql FROM sqlite_master WHERE type='index' ORDER BY tbl_name", null
        ).use { c -> buildList { while (c.moveToNext()) add(Triple(c.getString(0), c.getString(1), c.getString(2))) } }

        // 3. Para cada tabela, listar colunas
        val schemas = tables.associate { (tableName, _) ->
            tableName to db.rawQuery("PRAGMA table_info('$tableName')", null).use { c ->
                buildList {
                    while (c.moveToNext()) {
                        add(ColumnInfo(
                            cid = c.getInt(0),
                            name = c.getString(1),
                            type = c.getString(2),
                            notNull = c.getInt(3) == 1,
                            defaultValue = c.getStringOrNull(4),
                            isPrimaryKey = c.getInt(5) == 1
                        ))
                    }
                }
            }
        }

        // 4. Contar registros por tabela
        val rowCounts = tables.associate { (tableName, _) ->
            tableName to db.rawQuery("SELECT COUNT(*) FROM '$tableName'", null)
                .use { c -> if (c.moveToFirst()) c.getLong(0) else 0L }
        }

        // 5. Verificar modo de journal
        val journalMode = db.rawQuery("PRAGMA journal_mode", null)
            .use { c -> if (c.moveToFirst()) c.getString(0) else "unknown" }

        // 6. Verificar encoding
        val encoding = db.rawQuery("PRAGMA encoding", null)
            .use { c -> if (c.moveToFirst()) c.getString(0) else "unknown" }

        SQLiteInspectionResult(tables, indexes, schemas, rowCounts, journalMode, encoding)
    }
}

data class ColumnInfo(
    val cid: Int, val name: String, val type: String,
    val notNull: Boolean, val defaultValue: String?, val isPrimaryKey: Boolean
)

data class SQLiteInspectionResult(
    val tables: List<Pair<String, String>>,    // (name, type)
    val indexes: List<Triple<String, String, String?>>,
    val schemas: Map<String, List<ColumnInfo>>,
    val rowCounts: Map<String, Long>,
    val journalMode: String,
    val encoding: String
) {
    fun summary(): String = buildString {
        appendLine("=== SQLite Inspection ===")
        appendLine("Journal mode: $journalMode  |  Encoding: $encoding")
        appendLine("Tables (${tables.size}):")
        tables.forEach { (name, type) ->
            val cols = schemas[name]?.joinToString(", ") { it.name } ?: "?"
            val rows = rowCounts[name] ?: 0
            appendLine("  $name [$type] ($rows rows) — columns: $cols")
        }
    }
}
```

---

## 🔍 Passo 2 — Identificação de Tipo por Assinatura de Tabelas

```kotlin
object SQLiteModuleIdentifier {

    data class ModuleSignature(
        val requiredTables: Set<String>,
        val optionalTables: Set<String> = emptySet(),
        val identifyingColumns: Map<String, Set<String>> = emptyMap()
    )

    // Assinaturas conhecidas
    val SIGNATURES = mapOf(
        "MySword Bible" to ModuleSignature(
            requiredTables = setOf("Bible", "Details"),
            identifyingColumns = mapOf("Bible" to setOf("Book", "Chapter", "Verse", "Scripture"))
        ),
        "MySword Commentary" to ModuleSignature(
            requiredTables = setOf("Commentary", "Details"),
            identifyingColumns = mapOf("Commentary" to setOf("Book", "Chapter", "FromVerse", "ToVerse", "Data"))
        ),
        "MySword Dictionary" to ModuleSignature(
            requiredTables = setOf("Dictionary", "Details"),
            identifyingColumns = mapOf("Dictionary" to setOf("Word", "Data"))
        ),
        "MySword Journal/Notes" to ModuleSignature(
            requiredTables = setOf("Commentary"),
            optionalTables = setOf("Details")
            // Commentary sem Details = notes pessoais
        ),
        "MyBible Bible" to ModuleSignature(
            requiredTables = setOf("verses", "info"),  // minúsculas!
            identifyingColumns = mapOf("verses" to setOf("book_number", "chapter", "verse", "text"))
        ),
        "MyBible Commentary" to ModuleSignature(
            requiredTables = setOf("commentaries", "info"),
            identifyingColumns = mapOf("commentaries" to setOf("book_number", "chapter_number_from", "verse_number_from", "text"))
        ),
        "MyBible Dictionary" to ModuleSignature(
            requiredTables = setOf("dictionary", "info"),
            identifyingColumns = mapOf("dictionary" to setOf("topic", "definition"))
        ),
        "MySword Bookmarks" to ModuleSignature(
            requiredTables = setOf("Bookmarks")
        ),
        "MySword Highlights" to ModuleSignature(
            requiredTables = setOf("Highlights")
        ),
        "MySword Settings" to ModuleSignature(
            requiredTables = setOf("settings"),
            identifyingColumns = mapOf("settings" to setOf("key", "value"))
        ),
        "MySword Verselist" to ModuleSignature(
            requiredTables = setOf("Verselist")
        ),
        "MySword Tags" to ModuleSignature(
            requiredTables = setOf("Tags")
        )
    )

    fun identify(result: SQLiteInspectionResult): String {
        val tableNames = result.tables.map { it.first }.toSet()
        val tableNamesLower = tableNames.map { it.lowercase() }.toSet()

        return SIGNATURES.entries.firstOrNull { (_, sig) ->
            val required = sig.requiredTables
            val matchedTables = required.filter { req ->
                req in tableNames || req.lowercase() in tableNamesLower
            }
            if (matchedTables.size != required.size) return@firstOrNull false

            // Verificar colunas identificadoras
            sig.identifyingColumns.all { (tableName, expectedCols) ->
                val actualTable = tableNames.firstOrNull { it.equals(tableName, ignoreCase = true) }
                    ?: return@all false
                val actualCols = result.schemas[actualTable]?.map { it.name.lowercase() }?.toSet() ?: return@all false
                expectedCols.all { col -> col.lowercase() in actualCols }
            }
        }?.key ?: "Tipo desconhecido — tabelas: ${tableNames.joinToString()}"
    }
}
```

---

## 🛡️ Passo 3 — Leitura Defensiva (Schema Parcialmente Conhecido)

```kotlin
/**
 * Leitura defensiva: funciona mesmo que colunas opcionais estejam ausentes.
 * Sempre verificar PRAGMA table_info antes de SELECT de colunas desconhecidas.
 */
fun readTableDefensively(db: SQLiteDatabase, tableName: String): List<Map<String, Any?>> {
    // Verificar quais colunas realmente existem
    val existingColumns = db.rawQuery("PRAGMA table_info('$tableName')", null).use { c ->
        buildSet { while (c.moveToNext()) add(c.getString(1)) }  // coluna 'name'
    }

    if (existingColumns.isEmpty()) return emptyList()

    // Construir SELECT apenas com colunas existentes
    val selectCols = existingColumns.joinToString(", ") { "\"$it\"" }
    return db.rawQuery("SELECT $selectCols FROM \"$tableName\"", null).use { cursor ->
        buildList {
            while (cursor.moveToNext()) {
                val row = mutableMapOf<String, Any?>()
                existingColumns.forEachIndexed { idx, colName ->
                    row[colName] = when (cursor.getType(idx)) {
                        Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(idx)
                        Cursor.FIELD_TYPE_FLOAT   -> cursor.getDouble(idx)
                        Cursor.FIELD_TYPE_STRING  -> cursor.getString(idx)
                        Cursor.FIELD_TYPE_BLOB    -> cursor.getBlob(idx)
                        else -> null  // FIELD_TYPE_NULL
                    }
                }
                add(row)
            }
        }
    }
}

// Extensão para leitura segura de coluna opcional
fun Cursor.getStringOrNull(columnName: String): String? {
    val idx = getColumnIndex(columnName)
    return if (idx >= 0 && !isNull(idx)) getString(idx) else null
}

fun Cursor.getIntOrDefault(columnName: String, default: Int = 0): Int {
    val idx = getColumnIndex(columnName)
    return if (idx >= 0 && !isNull(idx)) getInt(idx) else default
}
```

---

## 💊 Passo 4 — Diagnóstico e Recovery de WAL Mode

```kotlin
/**
 * O MySword usa WAL (Write-Ahead Log) mode. Arquivos com .mybible-wal e .mybible-shm
 * podem estar em estado inconsistente se o app estava aberto quando o arquivo foi copiado.
 */
object WALDiagnostics {

    fun checkWALState(filePath: String): WALState {
        val walFile = File("$filePath-wal")
        val shmFile = File("$filePath-shm")
        return when {
            walFile.exists() && walFile.length() > 0 -> WALState.WAL_ACTIVE
            walFile.exists() && walFile.length() == 0L -> WALState.WAL_EMPTY
            shmFile.exists() -> WALState.SHM_ONLY
            else -> WALState.CLEAN
        }
    }

    /**
     * Forçar checkpoint do WAL (compactar WAL de volta para o arquivo principal).
     * Chamar SOMENTE se o arquivo não estiver sendo usado por outro processo.
     */
    fun forceWALCheckpoint(filePath: String): Boolean = runCatching {
        SQLiteDatabase.openDatabase(
            filePath, null,
            SQLiteDatabase.OPEN_READWRITE  // precisa de escrita para checkpoint
        ).use { db ->
            db.rawQuery("PRAGMA wal_checkpoint(FULL)", null).use { c ->
                c.moveToFirst()
                val busy = c.getInt(0)     // 0 = não busy
                val checkpointed = c.getInt(2)
                busy == 0 && checkpointed >= 0
            }
        }
    }.getOrDefault(false)

    /**
     * Converter de WAL para DELETE journal (mais seguro para arquivos read-only).
     * Use SOMENTE se você tem permissão de escrita e o arquivo não está em uso.
     */
    fun convertToDeleteMode(filePath: String): Boolean = runCatching {
        SQLiteDatabase.openDatabase(filePath, null, SQLiteDatabase.OPEN_READWRITE).use { db ->
            db.execSQL("PRAGMA journal_mode=DELETE")
            true
        }
    }.getOrDefault(false)

    enum class WALState { CLEAN, WAL_ACTIVE, WAL_EMPTY, SHM_ONLY }
}
```

---

## 🗃️ Guia dos Arquivos mydata/ com Schemas Inferidos

### bookmarks.mybible
```sql
-- Assinatura para identificação
-- Tabela obrigatória: Bookmarks
-- Colunas esperadas: Book, Chapter, Verse
-- Colunas opcionais: BibleMod, GroupName, Note, CreatedAt
SELECT name FROM sqlite_master WHERE name='Bookmarks'
```

### highlight.mybible
```sql
-- Tabela obrigatória: Highlights
-- Colunas esperadas: Book, Chapter, Verse, Color
-- Colunas opcionais: ToVerse, CreatedAt
-- Valores de Color: red, orange, brown, yellowgreen, green, bluegreen,
--                  blue, violet, purple, pink, gray
```

### settings.mybible
```sql
-- Estrutura key-value genérica
-- Tabela: settings
-- Colunas: key (TEXT PK), value (TEXT)
-- Atualizado frequentemente — não confiável para leitura durante uso do MySword
SELECT key, value FROM settings WHERE key LIKE '%current%' OR key LIKE '%bible%'
```

### format.mybible
```sql
-- Estrutura desconhecida — preservar opaco
-- NÃO tentar parsear FormatData — formato proprietário não documentado
-- Estratégia: copiar bytes sem interpretação ao fazer backup/restore
SELECT COUNT(*) FROM Format  -- apenas para verificar se tem dados
```

### default.xrefs.twm
```sql
-- Cross-references customizadas do usuário
-- Extensão .twm incomum mas internamente é SQLite
-- Assinatura provável: tabela de cross-references com book/chapter/verse
SELECT name FROM sqlite_master WHERE type='table'
```

---

## 🚨 Erros Comuns e Diagnóstico

| Erro | Causa provável | Diagnóstico |
|------|---------------|-------------|
| `no such table: Bible` | MySword com tabela minúscula | Verificar case: `bible` vs `Bible` |
| `no such column: scripture` | Coluna com nome diferente | `PRAGMA table_info('bible')` |
| `database disk image is malformed` | Arquivo corrompido ou WAL inconsistente | `PRAGMA integrity_check` |
| `attempt to write a readonly database` | Aberto como read-only para escrita | Mudar flags para `OPEN_READWRITE` |
| `unable to open database file` | Caminho errado ou sem permissão | Verificar `file.canRead()` antes |
| Dados vazios em todos os campos | Encoding UTF-16 vs UTF-8 | `PRAGMA encoding` |
| Números estranhos no `Book` | Módulo MyBible com PalmBible+ numbers | Converter via `MyBibleToMySword.bookMap` |
| `SQLiteFullException` | Disco cheio ao fazer checkpoint WAL | Liberar espaço, tentar checkpoint novamente |

---

## 🧪 Toolkit de Diagnóstico Rápido

```kotlin
// Uso: DumpSQLiteFile.dump(context, filePath) para diagnóstico completo em log
object DumpSQLiteFile {
    fun dump(filePath: String) {
        val walState = WALDiagnostics.checkWALState(filePath)
        android.util.Log.d("SQLiteForensics", "WAL state: $walState")

        val result = inspectSQLiteFile(filePath)
        android.util.Log.d("SQLiteForensics", result.summary())

        val moduleType = SQLiteModuleIdentifier.identify(result)
        android.util.Log.d("SQLiteForensics", "Identified as: $moduleType")

        // Amostra das primeiras 3 linhas de cada tabela
        SQLiteDatabase.openDatabase(filePath, null, SQLiteDatabase.OPEN_READONLY).use { db ->
            result.tables.forEach { (tableName, _) ->
                val sample = db.rawQuery("SELECT * FROM '$tableName' LIMIT 3", null)
                android.util.Log.d("SQLiteForensics", "  $tableName sample: ${sample.count} cols, first row: ${if (sample.moveToFirst()) (0 until sample.columnCount).map { sample.getString(it) } else "empty"}")
                sample.close()
            }
        }
    }
}
```

Files in this skill

  • SKILL.md15.5 KB
  • references/forensics.md1.6 KB
  • scripts/schema_diff.py4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…