From 6e8a0835083448e44c03f98ae329effa45f30aed Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Mon, 27 Dec 2021 00:09:57 +0100 Subject: [PATCH] WIP: sqlite3: Add decoder See sqlite3.{go,jq} for TODO Related to #27 --- format/all/all.go | 1 + format/format.go | 1 + format/sqlite3/sqlite3.go | 420 +++++++++++++++++++++++++++++++ format/sqlite3/sqlite3.jq | 61 +++++ format/sqlite3/testdata/test.db | Bin 0 -> 24576 bytes format/sqlite3/testdata/test.sh | 3 + format/sqlite3/testdata/test.sql | 18 ++ 7 files changed, 504 insertions(+) create mode 100644 format/sqlite3/sqlite3.go create mode 100644 format/sqlite3/sqlite3.jq create mode 100644 format/sqlite3/testdata/test.db create mode 100755 format/sqlite3/testdata/test.sh create mode 100644 format/sqlite3/testdata/test.sql diff --git a/format/all/all.go b/format/all/all.go index 782067193f..95ffa72a56 100644 --- a/format/all/all.go +++ b/format/all/all.go @@ -31,6 +31,7 @@ import ( _ "github.com/wader/fq/format/png" _ "github.com/wader/fq/format/protobuf" _ "github.com/wader/fq/format/raw" + _ "github.com/wader/fq/format/sqlite3" _ "github.com/wader/fq/format/tar" _ "github.com/wader/fq/format/tiff" _ "github.com/wader/fq/format/vorbis" diff --git a/format/format.go b/format/format.go index 80babc890d..5a9a2e58f3 100644 --- a/format/format.go +++ b/format/format.go @@ -79,6 +79,7 @@ const ( RAW = "raw" SLL_PACKET = "sll_packet" SLL2_PACKET = "sll2_packet" + SQLITE3 = "sqlite3" TAR = "tar" TCP_SEGMENT = "tcp_segment" TIFF = "tiff" diff --git a/format/sqlite3/sqlite3.go b/format/sqlite3/sqlite3.go new file mode 100644 index 0000000000..b7423161b5 --- /dev/null +++ b/format/sqlite3/sqlite3.go @@ -0,0 +1,420 @@ +package sqlite3 + +// https://www.sqlite.org/fileformat.html +// https://sqlite.org/src/file?name=src/btreeInt.h&ci=trunk +// https://sqlite.org/schematab.html + +// TODO: format version +// TODO: table/column names +// TODO: assert version and schema version? +// TODO: ptrmap +// TDOO: wal/journal files? combine? + +// > A table with the name "sqlite_sequence" that is used to keep track of the maximum historical INTEGER PRIMARY KEY for a table using AUTOINCREMENT. +// CREATE TABLE sqlite_sequence(name,seq); +// > Tables with names of the form "sqlite_statN" where N is an integer. Such tables store database statistics gathered by the ANALYZE command and used by the query planner to help determine the best algorithm to use for each query. +// CREATE TABLE sqlite_stat1(tbl,idx,stat); +// Only if compiled with SQLITE_ENABLE_STAT2: +// CREATE TABLE sqlite_stat2(tbl,idx,sampleno,sample); +// Only if compiled with SQLITE_ENABLE_STAT3: +// CREATE TABLE sqlite_stat3(tbl,idx,nEq,nLt,nDLt,sample); +// Only if compiled with SQLITE_ENABLE_STAT4: +// CREATE TABLE sqlite_stat4(tbl,idx,nEq,nLt,nDLt,sample); +// TODO: sqlite_autoindex_TABLE_N index + +import ( + "bytes" + "embed" + + "github.com/wader/fq/format" + "github.com/wader/fq/format/registry" + "github.com/wader/fq/internal/mathextra" + "github.com/wader/fq/pkg/bitio" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/scalar" +) + +//go:embed *.jq +var sqlite3FS embed.FS + +func init() { + registry.MustRegister(decode.Format{ + Name: format.SQLITE3, + Description: "SQLite v3 database", + Groups: []string{format.PROBE}, + DecodeFn: sqlite3Decode, + ToRepr: "_sqlite3_torepr", + Files: sqlite3FS, + }) +} + +const sqlite3HeaderSize = 100 + +const ( + serialTypeNULL = 0 + serialTypeS8 = 1 + serialTypeSBE16 = 2 + serialTypeSBE24 = 3 + serialTypeSBE32 = 4 + serialTypeSBE48 = 5 + serialTypeSBE64 = 6 + serialTypeFloatBE64 = 7 + serialTypeInteger0 = 8 + serialTypeInteger1 = 9 + serialTypeInternal10 = 10 + serialTypeInternal11 = 11 +) + +var serialTypeMap = scalar.SToSymStr{ + serialTypeNULL: "null", + serialTypeS8: "s8", + serialTypeSBE16: "sbe16", + serialTypeSBE24: "sbe24", + serialTypeSBE32: "sbe32", + serialTypeSBE48: "sbe48", + serialTypeSBE64: "sbe64", + serialTypeFloatBE64: "floatbe64", + serialTypeInteger0: "integer0", + serialTypeInteger1: "integer1", + serialTypeInternal10: "internal10", + serialTypeInternal11: "internal11", +} + +const ( + pageTypePtrmap = 0x00 + pageTypeBTreeIndexInterior = 0x02 + pageTypeBTreeTableInterior = 0x05 + pageTypeBTreeIndexLeaf = 0x0a + pageTypeBTreeTableLeaf = 0x0d +) + +var pageTypeMap = scalar.UToSymStr{ + // pageTypePtrmap: "ptrmap", + pageTypeBTreeIndexInterior: "index_interior", + pageTypeBTreeTableInterior: "table_interior", + pageTypeBTreeIndexLeaf: "index_leaf", + pageTypeBTreeTableLeaf: "table_leaf", +} + +var ptrmapTypeMap = scalar.UToSymStr{ + 1: "rootpage", + 2: "freepage", + 3: "overflow1", + 4: "overflow2", + 5: "btree", +} + +const ( + textEncodingUTF8 = 1 + textEncodingUTF16LE = 2 + textEncodingUTF16BE = 3 +) + +var textEncodingMap = scalar.UToSymStr{ + textEncodingUTF8: "utf8", + textEncodingUTF16LE: "utf16le", + textEncodingUTF16BE: "utf16be", +} + +var versionMap = scalar.UToSymStr{ + 1: "legacy", + 2: "wal", +} + +type sqlite3Header struct { + pageSize int64 + databaseSizePages int + textEncoding int +} + +// TODO: all bits if nine bytes? +// TODO: two complement on bit read count +func varintDecode(d *decode.D) int64 { + var n uint64 + for i := 0; i < 9; i++ { + v := d.U8() + n = n<<7 | v&0b0111_1111 + if v&0b1000_0000 == 0 { + break + } + } + return mathextra.TwosComplement(64, n) +} + +func sqlite3DecodeSerialType(d *decode.D, h sqlite3Header, typ int64) { + switch typ { + case serialTypeNULL: + d.FieldValueNil("value") + case serialTypeS8: + d.FieldS8("value") + case serialTypeSBE16: + d.FieldS16("value") + case serialTypeSBE24: + d.FieldS24("value") + case serialTypeSBE32: + d.FieldS32("value") + case serialTypeSBE48: + d.FieldS48("value") + case serialTypeSBE64: + d.FieldS64("value") + case serialTypeFloatBE64: + d.FieldF64("value") + case serialTypeInteger0: + d.FieldValueU("value", 0) + case serialTypeInteger1: + d.FieldValueU("value", 1) + case 10, 11: + // internal, should not appear in wellformed file + default: + if typ%2 == 0 { + // N => 12 and even: (N-12)/2 bytes blob. + d.FieldRawLen("value", (typ-12)/2*8) + } else { + // N => 13 and odd: (N-13)/2 bytes text + l := int(typ-13) / 2 + switch h.textEncoding { + case textEncodingUTF8: + d.FieldUTF8("value", l) + case textEncodingUTF16LE: + d.FieldUTF16LE("value", l) + case textEncodingUTF16BE: + d.FieldUTF16BE("value", l) + } + } + } +} + +func sqlite3DecodeCellFreeblock(d *decode.D) uint64 { + nextOffset := d.FieldU16("next_offset") + if nextOffset == 0 { + return 0 + } + // TODO: "header" is size bytes or offset+size? seems to be just size + // "size of the freeblock in bytes, including the 4-byte header" + size := d.FieldU16("size") + // TODO: really? + if size == 0 { + return 0 + } + d.FieldRawLen("space", int64(size-4)*8) + return nextOffset +} + +func sqlite3CellPayloadDecode(d *decode.D, h sqlite3Header) { + lengthStart := d.Pos() + length := d.FieldSFn("length", varintDecode) + lengthBits := d.Pos() - lengthStart + var serialTypes []int64 + d.LenFn((length)*8-lengthBits, func(d *decode.D) { + d.FieldArray("serials", func(d *decode.D) { + for !d.End() { + serialTypes = append(serialTypes, d.FieldSFn("serial", varintDecode, scalar.Fn(func(s scalar.S) (scalar.S, error) { + typ := s.ActualS() + if st, ok := serialTypeMap[typ]; ok { + s.Description = st + } else if typ >= 12 && typ%2 == 0 { + s.Description = "blob" + } else if typ >= 13 && typ%2 != 0 { + s.Description = "text" + } + + return s, nil + }))) + } + }) + }) + d.FieldArray("contents", func(d *decode.D) { + for _, s := range serialTypes { + sqlite3DecodeSerialType(d, h, s) + } + }) +} + +func sqlite3DecodeTreePage(d *decode.D, h sqlite3Header, x int64, payLoadLen int64) { + // formulas from sqlite format spec + u := h.pageSize + p := payLoadLen + m := ((u - 12) * 32 / 255) - 23 + k := m + ((p - m) % (u - 4)) + + var firstPayLoadLen int64 + if k <= x { + firstPayLoadLen = k + } else { + firstPayLoadLen = m + } + + if p <= x { + // payload fits in page + d.LenFn(firstPayLoadLen*8, func(d *decode.D) { + d.FieldStruct("payload", func(d *decode.D) { sqlite3CellPayloadDecode(d, h) }) + }) + } else { + // payload overflows, collect payload parts + payLoadBB := &bytes.Buffer{} + + d.FieldArray("overflow_pages", func(d *decode.D) { + var nextPage int64 + d.FieldStruct("overflow_page", func(d *decode.D) { + bib := d.FieldRawLen("data", firstPayLoadLen*8) + nextPage = d.FieldS32("next_page") + + bb, err := bib.Bytes() + if err != nil { + d.IOPanic(err, "first overflow page") + } + payLoadBB.Write(bb) + }) + + payLoadLenLeft := payLoadLen - firstPayLoadLen + for nextPage != 0 { + d.SeekAbs(((nextPage - 1) * h.pageSize) * 8) + d.FieldStruct("overflow_page", func(d *decode.D) { + nextPage = d.FieldS32("next_page") + overflowSize := mathextra.MinInt64(h.pageSize-4, payLoadLenLeft) + bib := d.FieldRawLen("data", overflowSize*8) + payLoadLenLeft -= overflowSize + + bb, err := bib.Bytes() + if err != nil { + d.IOPanic(err, "overflow page") + } + payLoadBB.Write(bb) + }) + } + }) + + d.FieldStructRootBitBufFn("payload", + bitio.NewBufferFromBytes(payLoadBB.Bytes(), -1), + func(d *decode.D) { sqlite3CellPayloadDecode(d, h) }, + ) + } +} + +func sqlite3Decode(d *decode.D, in interface{}) interface{} { + var h sqlite3Header + + d.FieldStruct("header", func(d *decode.D) { + d.FieldUTF8("magic", 16, d.AssertStr("SQLite format 3\x00")) + pageSizeS := d.FieldScalarU16("page_size", scalar.UToSymU{1: 65536}) // in bytes. Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536. + d.FieldU8("write_version", versionMap) // 1 for legacy; 2 for WAL. + d.FieldU8("read_version", versionMap) // . 1 for legacy; 2 for WAL. + d.FieldU8("unused_space") // at the end of each page. Usually 0. + d.FieldU8("maximum_embedded_payload_fraction") // . Must be 64. + d.FieldU8("minimum_embedded_payload_fraction") // . Must be 32. + d.FieldU8("leaf_payload_fraction") // . Must be 32. + d.FieldU32("file_change_counter") // + databaseSizePages := int(d.FieldU32("database_size_pages")) // . The "in-header database size". + d.FieldU32("page_number_freelist") // of the first freelist trunk page. + d.FieldU32("total_number_freelist") // pages. + d.FieldU32("schema_cookie") // . + d.FieldU32("schema_format_number") // . Supported schema formats are 1, 2, 3, and 4. + d.FieldU32("default_page_cache_size") // . + d.FieldU32("page_number_largest_root_btree") // page when in auto-vacuum or incremental-vacuum modes, or zero otherwise. + textEncoding := int(d.FieldU32("text_encoding", textEncodingMap)) + d.FieldU32("user_version") // " as read and set by the user_version pragma. + d.FieldU32("incremental_vacuum_mode") // False (zero) otherwise. + d.FieldU32("application_id") // " set by PRAGMA application_id. + d.FieldRawLen("reserved", 160, d.BitBufIsZero()) // for expansion. Must be zero. + d.FieldU32("version_valid_for") // number. + d.FieldU32("sqlite_version_number") // + + // TODO: nicer API for fallback? + pageSize := int64(pageSizeS.ActualU()) + if pageSizeS.Sym != nil { + pageSize = int64(pageSizeS.SymU()) + } + + h = sqlite3Header{ + pageSize: pageSize, + databaseSizePages: databaseSizePages, + textEncoding: textEncoding, + } + }) + + d.FieldArray("pages", func(d *decode.D) { + for i := 0; i < h.databaseSizePages; i++ { + pageOffset := h.pageSize * int64(i) + d.SeekAbs(pageOffset * 8) + // skip header for first page + if i == 0 { + d.SeekRel(sqlite3HeaderSize * 8) + } + + d.FieldStruct("page", func(d *decode.D) { + typ := d.FieldU8("type", pageTypeMap) + switch typ { + // case pageTypePtrmap: + // TODO: how to know if just a overflow page? + // log.Printf("ptrmap i: %#+v\n", i) + // d.FieldArray("entries", func(d *decode.D) { + // for j := int64(0); j < h.pageSize/5; j++ { + // d.FieldStruct("entry", func(d *decode.D) { + // d.FieldU8("type", ptrmapTypeMap) + // d.FieldU32("page_number") + // }) + // } + // }) + default: + d.FieldRawLen("data", (h.pageSize-4)*8) + + case pageTypeBTreeIndexInterior, pageTypeBTreeIndexLeaf, pageTypeBTreeTableInterior, pageTypeBTreeTableLeaf: + startFreeblocks := d.FieldU16("start_freeblocks") // The two-byte integer at offset 1 gives the start of the first freeblock on the page, or is zero if there are no freeblocks. + pageCells := d.FieldU16("page_cells") // The two-byte integer at offset 3 gives the number of cells on the page. + d.FieldU16("cell_start") // sThe two-byte integer at offset 5 designates the start of the cell content area. A zero value for this integer is interpreted as 65536. + d.FieldU8("cell_fragments") // The one-byte integer at offset 7 gives the number of fragmented free bytes within the cell content area. + + switch typ { + case pageTypeBTreeIndexInterior, + pageTypeBTreeTableInterior: + d.FieldU32("right_pointer") // The four-byte page number at offset 8 is the right-most pointer. This value appears in the header of interior b-tree pages only and is omitted from all other pages. + } + var cellPointers []uint64 + d.FieldArray("cells_pointers", func(d *decode.D) { + for i := uint64(0); i < pageCells; i++ { + cellPointers = append(cellPointers, d.FieldU16("pointer")) + } + }) + if startFreeblocks != 0 { + d.FieldArray("freeblocks", func(d *decode.D) { + nextOffset := startFreeblocks + for nextOffset != 0 { + d.SeekAbs((pageOffset + int64(nextOffset)) * 8) + d.FieldStruct("freeblock", func(d *decode.D) { + nextOffset = sqlite3DecodeCellFreeblock(d) + }) + } + }) + } + d.FieldArray("cells", func(d *decode.D) { + for _, p := range cellPointers { + d.FieldStruct("cell", func(d *decode.D) { + // TODO: SeekAbs with fn later? + d.SeekAbs((pageOffset + int64(p)) * 8) + switch typ { + case pageTypeBTreeIndexInterior: + d.FieldU32("left_child") + payLoadLen := d.FieldSFn("payload_len", varintDecode) + // formula for x from sqlite format spec + sqlite3DecodeTreePage(d, h, ((h.pageSize-12)*64/255)-23, payLoadLen) + case pageTypeBTreeTableInterior: + d.FieldU32("left_child") + d.FieldSFn("rowid", varintDecode) + case pageTypeBTreeIndexLeaf: + payLoadLen := d.FieldSFn("payload_len", varintDecode) + sqlite3DecodeTreePage(d, h, ((h.pageSize-12)*64/255)-23, payLoadLen) + case pageTypeBTreeTableLeaf: + payLoadLen := d.FieldSFn("payload_len", varintDecode) + d.FieldSFn("rowid", varintDecode) + sqlite3DecodeTreePage(d, h, h.pageSize-35, payLoadLen) + } + }) + } + }) + } + }) + } + }) + + return nil +} diff --git a/format/sqlite3/sqlite3.jq b/format/sqlite3/sqlite3.jq new file mode 100644 index 0000000000..121b925511 --- /dev/null +++ b/format/sqlite3/sqlite3.jq @@ -0,0 +1,61 @@ +# TODO: why page numbers-1? 0 excluded as special? + +# [ +# 12556, +# 12557, +# 12558 +# ] + +def sqlite3_btree_walk($page): + ( . as $root + | ( def _t: + if .type == "table_interior" or .type == "index_interior" then + ($root.pages[.cells[].left_child-1, .right_pointer-1 | if . == 2813 then 2812 end] | _t) + elif .type == "table_leaf" or .type == "index_leaf" then + .cells[] + else + error("unknown page type \(.type)") + end; + ($page | _t) + ) + ); + +# CREATE TABLE sqlite_schema( +# type text, +# name text, +# tbl_name text, +# rootpage integer, +# sql text +# ); +def sqlite3_schema: + ( [ sqlite3_btree_walk(.pages[0]) + | .payload.contents as [$type, $name, $tbl_name, $rootpage, $sql] + | { key: $name, + value: {$type, $name, $tbl_name, $rootpage, $sql} + } + ] + | from_entries + ); + +def sqlite3_rows($name): + ( sqlite3_schema[$name] as $s + | if $s == null then error("could not find name") end + | sqlite3_btree_walk(.pages[$s.rootpage-1]) + | . as {rowid: $rowid, payload: {$contents}} + | $contents + | tovalue + | if .[0] == null then .[0] = $rowid end + ); + +def _sqlite3_torepr: + ( . as $root + | sqlite3_schema + | map( + ( select(.type == "table") as $t + | { key: $t.name, + value: [$root | sqlite3_rows($t.name)] + } + ) + ) + | from_entries + ); diff --git a/format/sqlite3/testdata/test.db b/format/sqlite3/testdata/test.db new file mode 100644 index 0000000000000000000000000000000000000000..a6bc9ba53a7e5cc695ad1de1c15d5c35565cde5e GIT binary patch literal 24576 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