Index: Makefile.msc ================================================================== --- Makefile.msc +++ Makefile.msc @@ -1492,10 +1492,11 @@ $(TOP)\ext\misc\closure.c \ $(TOP)\ext\misc\csv.c \ $(TOP)\ext\misc\eval.c \ $(TOP)\ext\misc\fileio.c \ $(TOP)\ext\misc\fuzzer.c \ + $(TOP)\ext\zonefile\zonefile.c \ $(TOP)\ext\fts5\fts5_tcl.c \ $(TOP)\ext\fts5\fts5_test_mi.c \ $(TOP)\ext\fts5\fts5_test_tok.c \ $(TOP)\ext\misc\ieee754.c \ $(TOP)\ext\misc\mmapwarm.c \ ADDED ext/zonefile/README.md Index: ext/zonefile/README.md ================================================================== --- /dev/null +++ ext/zonefile/README.md @@ -0,0 +1,186 @@ + +# The Zonefile Extension + +## Functionality + +### Creating Zonefile Files + +To create a new zonefile, first create a database table with the following +schema: + +> CREATE TABLE data( +> k INTEGER PRIMARY KEY, +> frame INTEGER DEFAULT -1, -- frame number. Automatic if -1 +> idx INTEGER DEFAULT -1, -- index of entry within frame. Auto if -1 +> v BLOB +> ); + +The table may be created in a persistent or temporary database and may +take any name, but must contain the columns above. The table must be +populated with a row for each key intended to appear in the new zonefile +file. + +Once the table is populated, a zonefile is created using the following +SQL: + +> SELECT zonefile_write(, [, ]); + +where <file> is the name of the file to create on disk, <table> +is the name of the database table to read and optional argument +<parameters> is a JSON object containing various attributes that +influence creation of the zonefile file. + +Currently, the following <parameters> attributes are supported: + +
+
AttributeDefaultInterpretation +
maxAutoFrameSize65536 +The maximum uncompressed frame size in bytes for automatically generated +zonefile frames. + +
compressionTypeContent"none" +The compression type used to compress each frame in the zonefile. +Valid values are "none" (no compression), "zstd", "zstd_global_dict", +"zlib", "brotli", "lz4" and "lz4hc". Not all compression methods are +supported by all builds. The compression method supported by a build +depends on the combination of SQLITE_HAVE_ZSTD, SQLITE_HAVE_ZLIB, +SQLITE_HAVE_BROTLI and SQLITE_HAVE_LZ4 pre-processor symbols defined +at build time. + +
compressionTypeIndexData"none" +The compression type used to compress the zonefile index structure. +All values that are valid for the compressionTypeContent parameter, +except for "zstd_global_dict", are also valid for this option. + +
encryptionType"none" +The encryption type to use. At present the only valid values are +"none" (no encryption) and "xor" (an insecure mock encryption method +useful for testing only). Enhanced implementations may support any or +all of the following encryption schemes: +
    +
  • "AES_128_CTR" +
  • "AES_128_CBC" +
  • "AES_256_CTR" +
  • "AES_256_CBC" +
+ +
encryptionKey"" +The encryption key to use. The encryption key must be specified as an +even number of hexadecimal that will be converted to a binary key before +use. It is the responsibility of the caller to specify a key of the optimal +length for each encryption algorithm (e.g. 16 bytes (32 hex digits) for +a 128-bit encryption, or 32 bytes (64 digits) for a 256-bit method). +This option is ignored if encryptionType is set to "none". +
+ +For example, to create a zonefile named "test.zonefile" based on the +contents of database table "test_input", with a maximum automatic +frame size of 4096 bytes and using "xor" encryption with a 128-bit key: + +> SELECT zonefile_write('test.zonefile', 'test_input', +> '{"maxAutoFrameSize":4096, +> "encryptionType":"xor", +> "encryptionKey":"e6e600bc063aad12f6387beab650c48a" +> }' +> ); + +### Using (Reading) Zonefile Files + +To create a new zonefile table, one of the following: + +> CREATE VIRTUAL TABLE z1 USING zonefile; +> CREATE VIRTUAL TABLE z1 USING zonefile(cachesize=N); + +where N is any non-zero positive integer. If the zonefile is used +to access any files containing compressed or encrypted data, it maintains +an LRU cache of uncompressed frame data N frames in size. The +default value of N is 1. + +Creating a "zonefile" virtual table actually creates two virtual tables in the +database schema. One read-only table named "z1", with a schema equivalent to: + +> CREATE TABLE z1( -- this whole table is read-only +> k INTEGER PRIMARY KEY, -- key value +> v BLOB, -- associated blob of data +> fileid INTEGER, -- file id (rowid value for z1_files) +> sz INTEGER -- size of blob of data in bytes +> ); + +And a read-write table named "z1_files" with a schema like: + +> CREATE TABLE z1_files( +> filename TEXT PRIMARY KEY, +> ekey BLOB, -- encryption key +> header JSON HIDDEN -- read-only +> ); + +Both tables are initially empty. To add a zonefile to the index, insert a +row into the "z1_files" table: + +> INSERT INTO z1_files(filename) VALUES(); + +If the file is an encrypted file, then the encryption key (a blob) must +be inserted into the "ekey" column. Encryption keys are not stored in the +database, they are held in main-memory only. This means that each new +connection must configure encryption key using UPDATE statements before +accessing any encrypted files. For example: + +> -- Add new encrypted file to database: +> INSERT INTO z1_files(filename, ekey) VALUES(, ); +> +> -- Configure encryption key for existing file after opening database: +> UPDATE z1_files SET ekey = WHERE filename = ; + +Currently, values provided for any columns other than "filename" and +"ekey" are ignored. Files are removed from the index by deleting rows +from the z1_files table: + +> DELETE FROM z1_files WHERE filename = ; + +Once zonefile files have been added to the index, their contents are +visible in table "z1". To retrieve the value associated with a single +key from one of the zonefile files in the index: + +> SELECT v FROM z1 WHERE k = ; + + +## Notes + + * Contrary to the spec, the implementation uses 32-bit (not 16-bit) frame + numbers. So the KeyOffsetPair structure becomes: + + KeyOffsetPair + { + uint64 key; + uint32 frameNo; + uint32 frameByteOffset; + }; + + Also, the ZonefileHeader.numFrames field is now 32-bit. Which makes + the ZonefileHeader structure 26 bytes in size. The implementation + pads this out to 32 bytes so that the ZoneFileIndex is 8-byte aligned. + + * Multi-byte integer values are big-endian. + + * The offsets in the ZoneFileIndex.byteOffsetZoneFrame[] array are + relative to the offset in ZoneFileHeader.byteOffsetFrames. This is + necessary as we may not know the offset of the start of the frame data + until after the ZoneFileIndex structure is compressed. + + * The offsets in the ZoneFileIndex.byteOffsetZoneFrame[] array are the + offsets for the first byte past the end of the corresponding frame. + For example, byteOffsetZoneFrame[] identifies the first byte of the + second frame, and byteOffsetZoneFrame[numFrames-1] is one byte past + the end of the last frame in the file. + + This is better as if we store the starting offset of each frame, there + is no way to determine the size of the last frame in the file without + trusting the filesize itself. + + * Currently there is no support at all for encryption. + + * Zonefile currently uses json1 to parse the json argument to + zonefile\_write(). And so must be used with an SQLITE\_ENABLE\_JSON1 + or otherwise json1-enabled SQLite. + + ADDED ext/zonefile/zonefile.c Index: ext/zonefile/zonefile.c ================================================================== --- /dev/null +++ ext/zonefile/zonefile.c @@ -0,0 +1,3083 @@ +/* +** 2018-02-10 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +****************************************************************************** +*/ + +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1 +#ifndef SQLITE_OMIT_VIRTUALTABLE + +#include +#include +#include +#include + +/* +** Default values for various zonefile_write() parameters. +*/ +#define ZONEFILE_DEFAULT_MAXAUTOFRAMESIZE (64*1024) +#define ZONEFILE_DEFAULT_ENCRYPTION 1 +#define ZONEFILE_DEFAULT_COMPRESSION 0 +#define ZONEFILE_DEFAULT_DICTSIZE (64*1024) + +/* +** Value to use for the first 4 bytes of a zonefile file header. +*/ +#define ZONEFILE_MAGIC_NUMBER 0x464B3138 + +/* +** Size of a zonefile header. And of each entry in the +** ZonefileIndex.keyOffsets array. +*/ +#define ZONEFILE_SZ_HEADER 32 +#define ZONEFILE_SZ_KEYOFFSETS_ENTRY 20 + +/* +** Constants for supported compression types. These are copied from the +** published file format spec. +*/ +#define ZONEFILE_COMPRESSION_NONE 0 +#define ZONEFILE_COMPRESSION_ZSTD 1 +#define ZONEFILE_COMPRESSION_ZSTD_GLOBAL_DICT 2 +#define ZONEFILE_COMPRESSION_ZLIB 3 +#define ZONEFILE_COMPRESSION_BROTLI 4 +#define ZONEFILE_COMPRESSION_LZ4 5 +#define ZONEFILE_COMPRESSION_LZ4HC 6 + +/* +** Schema for "zonefile" virtual table. +*/ +#define ZONEFILE_SCHEMA \ + "CREATE TABLE z1(" \ + " k INTEGER PRIMARY KEY," \ + " v BLOB," \ + " fileid INTEGER," \ + " sz INTEGER" \ + ")" + +/* +** Schema for "zonefile_files" virtual table. +*/ +#define ZONEFILE_FILES_SCHEMA \ + "CREATE TABLE z2(" \ + " filename TEXT," \ + " ekey BLOB," \ + " header JSON HIDDEN" \ + ")" + + +#ifndef SQLITE_AMALGAMATION +typedef sqlite3_int64 i64; +typedef sqlite3_uint64 u64; +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned long u32; +#define MIN(a,b) ((a)<(b) ? (a) : (b)) + +#if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST) +# define ALWAYS(X) (1) +# define NEVER(X) (0) +#elif !defined(NDEBUG) +# define ALWAYS(X) ((X)?1:(assert(0),0)) +# define NEVER(X) ((X)?(assert(0),1):0) +#else +# define ALWAYS(X) (X) +# define NEVER(X) (X) +#endif +#endif /* SQLITE_AMALGAMATION */ + +/* +** Forward declarations for encryption/decryption functions. +** +** If this module is not compiled with SQLITE_HAVE_ZONEFILE_CODEC, then +** implementations of the following type and functions that support the +** mock encryption method "xor" only are provided. Alternatively, the +** application may append a more functional implementation of the following +** type and functions to this file before compiling it with +** SQLITE_HAVE_ZONEFILE_CODEC defined. +*/ +typedef struct ZonefileCodec ZonefileCodec; +static int zonefileCodecCreate( + int,int,unsigned char*,int,ZonefileCodec**,char**); +static int zonefileCodecNonceSize(ZonefileCodec*); +static void zonefileCodecEncode(ZonefileCodec*, unsigned char*, int); +static void zonefileCodecDecode(ZonefileCodec*, unsigned char*, int); +static void zonefileCodecDestroy(ZonefileCodec*); + +#ifndef SQLITE_HAVE_ZONEFILE_CODEC +typedef struct ZonefileCodec ZonefileCodec; + +struct ZonefileCodec { + u8 aKey[16]; + int bEncrypt; /* Second parameter passed to Create() */ +}; + +/* Create a new encryption module instance using algorithm iAlg. +** +** iAlg==1 AES128 CTR +** iAlg==2 AES128 CBC +** iAlg==3 AES256 CTR +** iAlg==4 AES256 CBC +** iAlg==5 XOR Testing use only +** +** If the requested algorithm is not available, the routine returns +** a NULL pointer. NULL is also returned on a OOM error. +** +** Use zonefileCodecDestroy() to reclaim memory. +*/ +static int zonefileCodecCreate( + int iAlg, + int bEncrypt, /* True for encryption, zero for decryption */ + unsigned char *pKey, int nKey, + ZonefileCodec **pp, + char **pzErr +){ + ZonefileCodec *pRet; + int rc = SQLITE_OK; + + if( iAlg!=5 ){ + *pzErr = sqlite3_mprintf("unsupported encryption method: %d", iAlg); + rc = SQLITE_ERROR; + }else{ + *pp = pRet = (ZonefileCodec*)sqlite3_malloc(sizeof(ZonefileCodec)); + if( pRet==0 ){ + rc = SQLITE_NOMEM; + }else{ + int i; + for(i=0; iaKey); i++){ + pRet->aKey[i] = pKey[i % nKey]; + } + pRet->bEncrypt = bEncrypt; + } + } + + return rc; +} + +/* Return the size of the nonce used for the given encryption module */ +static int zonefileCodecNonceSize(ZonefileCodec *pCodec){ + return 16; +} + +/* Encrypt in-place. +** +** The size of the content will grow by the nonce size. Hence, the +** buffer must have at least nonce bytes of extra space available at +** the end to accommodate that growth. When persisting results, be +** sure to include the extra bytes. +*/ +static void zonefileCodecEncode( + ZonefileCodec *pCodec, + unsigned char *pIn, int nIn +){ + int i; + u8 *aNonce = &pIn[nIn]; + assert( pCodec->bEncrypt ); + sqlite3_randomness(16, aNonce); + for(i=0; iaKey[i%16]; + } +} + +/* Decrypt in-place. +** +** The size of the decrypted text will be less than the input buffer +** by nonce-size bytes. +*/ +static void zonefileCodecDecode( + ZonefileCodec *pCodec, + unsigned char *pIn, int nIn +){ + int i; + u8 *aNonce = &pIn[nIn-16]; + assert( pCodec->bEncrypt==0 ); + for(i=0; iaKey[i%16]; + } +} + +/* Destroy an encryption module. +** It is harmless to pass in a NULL pointer. +*/ +static void zonefileCodecDestroy(ZonefileCodec *pCodec){ + sqlite3_free(pCodec); +} +#endif /* SQLITE_HAVE_ZONEFILE_CODEC */ + +/* +** All zonefile and zonefile_files virtual table instances that belong +** to the same database handle (sqlite3*) share a single instance of the +** ZonefileGlobal object. This global object contains a table of +** configured encryption keys for the various zonefiles in the system. +*/ +typedef struct ZonefileGlobal ZonefileGlobal; +typedef struct ZonefileKey ZonefileKey; +struct ZonefileGlobal { + int nEntry; /* Number of entries in the hash table */ + int nHash; /* Size of aHash[] array */ + ZonefileKey **aHash; /* Hash buckets */ +}; +struct ZonefileKey { + const char *zName; /* Zonefile table name */ + const char *zDb; /* Database name ("main", "temp" etc.) */ + i64 iFileid; /* File id */ + const u8 *aKey; /* Key buffer */ + int nKey; /* Size of zKey in bytes */ + u32 iHash; /* zonefileKeyHash() value */ + ZonefileKey *pHashNext; /* Next colliding key in hash table */ +}; + +/* +** Return a 32-bit hash value for the three arguments. +*/ +static u32 zonefileKeyHash( + const char *zDb, + const char *zTab, + i64 iFileid +){ + u32 iHash = 0; + int i; + for(i=0; zDb[i]; i++) iHash += (iHash<<3) + (u8)zDb[i]; + for(i=0; zTab[i]; i++) iHash += (iHash<<3) + (u8)zTab[i]; + return (iHash ^ (iFileid & 0xFFFFFFFF)); +} + +/* +** Store encryption key aKey in the key-store passed as the first argument. +** Return SQLITE_OK if successful, or an SQLite error code (SQLITE_NOMEM) +** otherwise. +*/ +static int zonefileKeyStore( + ZonefileGlobal *pGlobal, + const char *zDb, /* Database containing zonefile table */ + const char *zTab, /* Name of zonefile table */ + i64 iFileid, /* File-id to configure key for */ + const u8 *aKey, /* Key to store */ + int nKey /* Size of aKey[] in bytes */ +){ + ZonefileKey **pp; + u32 iHash = zonefileKeyHash(zDb, zTab, iFileid); + + /* Remove any old entry */ + if( pGlobal->nHash ){ + for(pp=&pGlobal->aHash[iHash%pGlobal->nHash]; *pp; pp=&((*pp)->pHashNext)){ + ZonefileKey *pThis = *pp; + if( pThis->iFileid==iFileid + && 0==sqlite3_stricmp(zTab, pThis->zName) + && 0==sqlite3_stricmp(zDb, pThis->zDb) + ){ + pGlobal->nEntry--; + *pp = pThis->pHashNext; + sqlite3_free(pThis); + break; + } + } + } + + if( aKey ){ + int nDb = strlen(zDb); + int nTab = strlen(zTab); + ZonefileKey *pNew; + + /* Resize the hash-table, if necessary */ + if( pGlobal->nEntry>=pGlobal->nHash ){ + int i; + int n = pGlobal->nHash ? pGlobal->nHash*2 : 16; + ZonefileKey **a = (ZonefileKey**)sqlite3_malloc(n*sizeof(ZonefileKey*)); + if( a==0 ) return SQLITE_NOMEM; + memset(a, 0, n*sizeof(ZonefileKey*)); + for(i=0; inHash; i++){ + ZonefileKey *p; + ZonefileKey *pNext; + for(p=pGlobal->aHash[i]; p; p=pNext){ + pNext = p->pHashNext; + p->pHashNext = a[p->iHash % n]; + a[p->iHash % n] = p; + } + } + sqlite3_free(pGlobal->aHash); + pGlobal->aHash = a; + pGlobal->nHash = n; + } + + pNew = (ZonefileKey*)sqlite3_malloc( + sizeof(ZonefileKey) + nKey+1 + nDb+1 + nTab+1 + ); + if( pNew==0 ) return SQLITE_NOMEM; + memset(pNew, 0, sizeof(ZonefileKey)); + pNew->iFileid = iFileid; + pNew->iHash = iHash; + pNew->aKey = (const u8*)&pNew[1]; + pNew->nKey = nKey; + pNew->zDb = (const char*)&pNew->aKey[nKey+1]; + pNew->zName = &pNew->zDb[nDb+1]; + memcpy((u8*)pNew->aKey, aKey, nKey+1); + memcpy((char*)pNew->zDb, zDb, nDb+1); + memcpy((char*)pNew->zName, zTab, nTab+1); + + pNew->pHashNext = pGlobal->aHash[iHash % pGlobal->nHash]; + pGlobal->aHash[iHash % pGlobal->nHash] = pNew; + pGlobal->nEntry++; + } + + return SQLITE_OK; +} + +/* +** Search the key-store passed as the first argument for an encryption +** key to use with the file with file-id iFileid in zonefile table zTab +** in database zDb. If successful, set (*paKey) to point to the key +** buffer and return the size of the key in bytes. +** +** If no key is found, return 0. The final value of (*paKey) is undefined +** in this case. +*/ +static int zonefileKeyFind( + ZonefileGlobal *pGlobal, + const char *zDb, /* Database containing zonefile table */ + const char *zTab, /* Name of zonefile table */ + i64 iFileid, /* File-id to configure key for */ + const u8 **paKey /* OUT: Pointer to key buffer */ +){ + if( pGlobal->nHash ){ + ZonefileKey *pKey; + u32 iHash = zonefileKeyHash(zDb, zTab, iFileid); + for(pKey=pGlobal->aHash[iHash%pGlobal->nHash]; pKey; pKey=pKey->pHashNext){ + if( pKey->iFileid==iFileid + && 0==sqlite3_stricmp(zTab, pKey->zName) + && 0==sqlite3_stricmp(zDb, pKey->zDb) + ){ + *paKey = pKey->aKey; + return pKey->nKey; + } + } + } + + return 0; +} + +/* +** The pointer passed as the only argument must actually point to a +** ZonefileGlobal structure. This function frees the structure and all +** of its components. +*/ +static void zonefileKeyDestroy(void *p){ + ZonefileGlobal *pGlobal = (ZonefileGlobal*)p; + int i; + for(i=0; inHash; i++){ + ZonefileKey *pKey; + ZonefileKey *pNext; + for(pKey=pGlobal->aHash[i]; pKey; pKey=pNext){ + pNext = pKey->pHashNext; + sqlite3_free(pKey); + } + } + sqlite3_free(pGlobal->aHash); + sqlite3_free(pGlobal); +} + +/* +** Write value v to buffer aBuf as an unsigned 32-bit big-endian integer. +*/ +static void zonefilePut32(u8 *aBuf, u32 v){ + aBuf[0] = (v >> 24) & 0xFF; + aBuf[1] = (v >> 16) & 0xFF; + aBuf[2] = (v >> 8) & 0xFF; + aBuf[3] = v & 0xFF; +} + +/* +** Read and return an unsigned 32-bit big-endian integer from buffer aBuf. +*/ +static u32 zonefileGet32(const u8 *aBuf){ + return (((u32)aBuf[0]) << 24) + + (((u32)aBuf[1]) << 16) + + (((u32)aBuf[2]) << 8) + + (((u32)aBuf[3]) << 0); +} + +/* +** Generic xOpen, xClose and xUncompressSize methods used by a few +** different compression method bindings. +*/ +static int zfGenericOpen(void **pp, u8 *aDict, int nDict){ + *pp = 0; + return SQLITE_OK; +} +static void zfGenericClose(void *p){ +} +static int zfGenericUncompressSize( + void *p, + const u8 *aSrc, int nSrc +){ + return (int)zonefileGet32(aSrc); +} + +#ifdef SQLITE_HAVE_ZLIB +#include +static int zfZlibCompressBound(void *p, int nSrc){ + return (int)compressBound((uLong)nSrc) + 4; +} +static int zfZlibCompress( + void *p, + u8 *aDest, int *pnDest, + const u8 *aSrc, int nSrc +){ + uLongf destLen = (uLongf)(*pnDest)-4; + int rc = compress(&aDest[4], &destLen, aSrc, (uLong)nSrc); + *pnDest = (int)(destLen+4); + zonefilePut32(aDest, nSrc); + return rc==Z_OK ? SQLITE_OK : SQLITE_ERROR; +} +static int zfZlibUncompress( + void *p, + u8 *aDest, int nDest, + const u8 *aSrc, int nSrc +){ + uLongf destLen = (uLongf)nDest; + int rc = uncompress(aDest, &destLen, &aSrc[4], (uLong)nSrc-4); + return rc==Z_OK ? SQLITE_OK : SQLITE_ERROR; +} +#endif +#ifdef SQLITE_HAVE_ZSTD +#include +static int zfZstdCompressBound(void *p, int nSrc){ + return (int)ZSTD_compressBound((size_t)nSrc); +} +static int zfZstdCompress( + void *p, + u8 *aDest, int *pnDest, + const u8 *aSrc, int nSrc +){ + size_t szDest = (size_t)(*pnDest); + size_t rc = ZSTD_compress(aDest, szDest, aSrc, (size_t)nSrc, 1); + if( ZSTD_isError(rc) ) return SQLITE_ERROR; + *pnDest = (int)rc; + return SQLITE_OK; +} +static int zfZstdUncompressSize(void *p, const u8 *aSrc, int nSrc){ + return (int)ZSTD_getFrameContentSize(aSrc, (size_t)nSrc); +} +static int zfZstdUncompress( + void *p, + u8 *aDest, int nDest, + const u8 *aSrc, int nSrc +){ + size_t rc = ZSTD_decompress(aDest, (size_t)nDest, aSrc, (size_t)nSrc); + if( rc!=(size_t)nDest ) return SQLITE_ERROR; + return SQLITE_OK; +} + +#include +typedef struct ZfZstddict ZfZstddict; +struct ZfZstddict { + ZSTD_CDict *pCDict; + ZSTD_CCtx *pCCtx; + ZSTD_DDict *pDDict; + ZSTD_DCtx *pDCtx; +}; + +static void zfZstddictClose(void *p){ + if( p ){ + ZfZstddict *pCmp = (ZfZstddict*)p; + if( pCmp->pCDict ) ZSTD_freeCDict(pCmp->pCDict); + if( pCmp->pCCtx ) ZSTD_freeCCtx(pCmp->pCCtx); + if( pCmp->pDCtx ) ZSTD_freeDCtx(pCmp->pDCtx); + sqlite3_free(pCmp); + } +} +static int zfZstddictOpen(void **pp, u8 *aDict, int nDict){ + int rc = SQLITE_OK; + ZfZstddict *pDict = (ZfZstddict*)sqlite3_malloc(sizeof(ZfZstddict)); + if( pDict==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(pDict, 0, sizeof(ZfZstddict)); + if( aDict ){ + pDict->pDDict = ZSTD_createDDict(aDict, nDict); + pDict->pDCtx = ZSTD_createDCtx(); + if( pDict->pDDict==0 || pDict->pDCtx==0 ){ + zfZstddictClose((void*)pDict); + pDict = 0; + rc = SQLITE_ERROR; + } + } + } + *pp = (void*)pDict; + return rc; +} +static int zfZstddictTrain( + void *p, /* Compressor handle */ + u8 *aDict, int *pnDict, /* OUT: Dictionary buffer */ + u8 *aSamp, size_t *aSz, int nSamp /* IN: Training samples */ +){ + ZfZstddict *pCmp = (ZfZstddict*)p; + size_t sz = ZDICT_trainFromBuffer(aDict, (size_t)*pnDict, aSamp, aSz, nSamp); + if( ZDICT_isError(sz) ) return SQLITE_ERROR; + pCmp->pCDict = ZSTD_createCDict(aDict, sz, 1); + pCmp->pCCtx = ZSTD_createCCtx(); + if( pCmp->pCDict==0 || pCmp->pCCtx==0 ) return SQLITE_ERROR; + *pnDict = (int)sz; + return SQLITE_OK; +} +static int zfZstddictCompress( + void *p, + u8 *aDest, int *pnDest, + const u8 *aSrc, int nSrc +){ + ZfZstddict *pCmp = (ZfZstddict*)p; + size_t szDest = (size_t)(*pnDest); + size_t rc; + assert( pCmp && pCmp->pCDict && pCmp->pCCtx ); + rc = ZSTD_compress_usingCDict( + pCmp->pCCtx, aDest, szDest, aSrc, (size_t)nSrc, pCmp->pCDict + ); + if( ZSTD_isError(rc) ) return SQLITE_ERROR; + *pnDest = (int)rc; + return SQLITE_OK; +} +static int zfZstddictUncompress( + void *p, + u8 *aDest, int nDest, + const u8 *aSrc, int nSrc +){ + ZfZstddict *pCmp = (ZfZstddict*)p; + size_t rc = ZSTD_decompress_usingDDict( + pCmp->pDCtx, aDest, (size_t)nDest, aSrc, (size_t)nSrc, pCmp->pDDict + ); + if( rc!=(size_t)nDest ) return SQLITE_ERROR; + return SQLITE_OK; +} +#endif + +#ifdef SQLITE_HAVE_LZ4 +#include +#include +static int zfLz4CompressBound(void *p, int nSrc){ + return (int)LZ4_compressBound(nSrc) + 4; +} +static int zfLz4Uncompress( + void *p, + u8 *aDest, int nDest, + const u8 *aSrc, int nSrc +){ + int rc = LZ4_decompress_safe( + (const char*)&aSrc[4], (char*)aDest, nSrc-4, nDest + ); + return rc==nDest ? SQLITE_OK : SQLITE_ERROR; +} +static int zfLz4Compress( + void *p, + u8 *aDest, int *pnDest, + const u8 *aSrc, int nSrc +){ + int rc = LZ4_compress_default( + (const char*)aSrc, (char*)&aDest[4], nSrc, (*pnDest - 4) + ); + *pnDest = rc+4; + zonefilePut32(aDest, nSrc); + return rc==0 ? SQLITE_ERROR : SQLITE_OK; +} +static int zfLz4hcCompress( + void *p, + u8 *aDest, int *pnDest, + const u8 *aSrc, int nSrc +){ + int rc = LZ4_compress_HC( + (const char*)aSrc, (char*)&aDest[4], nSrc, *pnDest, 0 + ); + *pnDest = rc+4; + zonefilePut32(aDest, nSrc); + return rc==0 ? SQLITE_ERROR : SQLITE_OK; +} +#endif + +#ifdef SQLITE_HAVE_BROTLI +#include +#include + +static int zfBrotliCompressBound(void *p, int nSrc){ + return (int)BrotliEncoderMaxCompressedSize((size_t)nSrc) + 4; +} +static int zfBrotliCompress( + void *p, + u8 *aDest, int *pnDest, + const u8 *aSrc, int nSrc +){ + size_t nDest = (size_t)*pnDest - 4; + BROTLI_BOOL rc = BrotliEncoderCompress( + BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, + (size_t)nSrc, aSrc, &nDest, &aDest[4] + ); + *pnDest = (int)nDest + 4; + zonefilePut32(aDest, nSrc); + return rc==0 ? SQLITE_ERROR : SQLITE_OK; +} +static int zfBrotliUncompress( + void *p, + u8 *aDest, int nDest, + const u8 *aSrc, int nSrc +){ + size_t n = nDest; + BrotliDecoderResult rc = BrotliDecoderDecompress(nSrc-4, &aSrc[4], &n, aDest); + return rc==BROTLI_DECODER_RESULT_SUCCESS ? SQLITE_OK : SQLITE_ERROR; +} +#endif + +typedef struct ZonefileCompress ZonefileCompress; +static struct ZonefileCompress { + int eType; + const char *zName; + int (*xOpen)(void**, u8 *aDict, int nDict); + void (*xClose)(void*); + int (*xTrain)(void*, u8 *aDict, int *pnDict, u8 *a, size_t *aSz, int n); + int (*xCompressBound)(void*, int nSrc); + int (*xCompress)(void*, u8 *aDest, int *pnDest, const u8 *aSrc, int nSrc); + int (*xUncompressSize)(void*, const u8 *aSrc, int nSrc); + int (*xUncompress)(void*, u8 *aDest, int nDest, const u8 *aSrc, int nSrc); +} aZonefileCompress[] = { + { ZONEFILE_COMPRESSION_NONE, "none", + 0, 0, 0, 0, 0, 0, 0 + }, +#ifdef SQLITE_HAVE_ZSTD + { ZONEFILE_COMPRESSION_ZSTD, "zstd", + zfGenericOpen, zfGenericClose, + 0, + zfZstdCompressBound, zfZstdCompress, + zfZstdUncompressSize, zfZstdUncompress + }, + { ZONEFILE_COMPRESSION_ZSTD_GLOBAL_DICT, "zstd_global_dict", + zfZstddictOpen, zfZstddictClose, + zfZstddictTrain, + zfZstdCompressBound, zfZstddictCompress, + zfZstdUncompressSize, zfZstddictUncompress + }, +#endif /* SQLITE_HAVE_ZSTD */ +#ifdef SQLITE_HAVE_ZLIB + { ZONEFILE_COMPRESSION_ZLIB, "zlib", + zfGenericOpen, zfGenericClose, + 0, + zfZlibCompressBound, zfZlibCompress, + zfGenericUncompressSize, zfZlibUncompress + }, +#endif /* SQLITE_HAVE_ZLIB */ +#ifdef SQLITE_HAVE_BROTLI + { ZONEFILE_COMPRESSION_BROTLI, "brotli", + zfGenericOpen, zfGenericClose, + 0, + zfBrotliCompressBound, zfBrotliCompress, + zfGenericUncompressSize, zfBrotliUncompress + }, +#endif /* SQLITE_HAVE_BROTLI */ +#ifdef SQLITE_HAVE_LZ4 + { ZONEFILE_COMPRESSION_LZ4, "lz4", + zfGenericOpen, zfGenericClose, + 0, + zfLz4CompressBound, zfLz4Compress, + zfGenericUncompressSize, zfLz4Uncompress + }, + { ZONEFILE_COMPRESSION_LZ4HC, "lz4hc", + zfGenericOpen, zfGenericClose, + 0, + zfLz4CompressBound, zfLz4hcCompress, + zfGenericUncompressSize, zfLz4Uncompress + }, +#endif /* SQLITE_HAVE_LZ4 */ +}; + +/* +** Find the ZonefileCompress object for the compression scheme named +** by zName. If successful, set output variable (*pp) to point to the +** object and return SQLITE_OK. Otherwise, return SQLITE_ERROR and +** leave output variable (*pzErr) pointing to an English language error +** message. It is the reponsibility of the caller to eventually free +** the error message buffer using sqlite3_free(). +*/ +static int zonefileCompress( + const char *zName, /* Name of requested compression method */ + ZonefileCompress **pp, /* OUT: Pointer to compression object */ + char **pzErr /* OUT: Error message */ +){ + int i; + assert( *pzErr==0 ); + for(i=0; imaxAutoFrameSize = ZONEFILE_DEFAULT_MAXAUTOFRAMESIZE; + p->encryptionType = ZONEFILE_DEFAULT_ENCRYPTION; + p->pCmpData = p->pCmpIdx = zonefileCompressByValue(ZONEFILE_COMPRESSION_NONE); + + rc = zonefilePrepare(db, &pStmt, &zErr,"SELECT key, value FROM json_each(?)"); + if( rc==SQLITE_OK ){ + sqlite3_bind_text(pStmt, 1, zJson, -1, SQLITE_STATIC); + } + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zKey = (const char*)sqlite3_column_text(pStmt, 0); + int iVal = sqlite3_column_int(pStmt, 1); + if( sqlite3_stricmp("debugExtendedHeaderSize", zKey)==0 ){ + if( iVal<0 || iVal>255 ){ + zErr = sqlite3_mprintf( + "debugExtendedHeaderSize value out of range: %d", iVal + ); + rc = SQLITE_ERROR; + } + p->debugExtendedHeaderSize = iVal; + }else + if( sqlite3_stricmp("debugEncryptionKeyText", zKey)==0 ){ + p->debugEncryptionKeyText = iVal; + }else + if( sqlite3_stricmp("maxAutoFrameSize", zKey)==0 ){ + p->maxAutoFrameSize = iVal; + }else + if( sqlite3_stricmp("compressionTypeIndexData", zKey)==0 ){ + const char *zName = (const char*)sqlite3_column_text(pStmt, 1); + rc = zonefileCompress(zName, &p->pCmpIdx, &zErr); + }else + if( sqlite3_stricmp("compressionTypeContent", zKey)==0 ){ + const char *zName = (const char*)sqlite3_column_text(pStmt, 1); + rc = zonefileCompress(zName, &p->pCmpData, &zErr); + }else + if( sqlite3_stricmp("encryptionKey", zKey)==0 ){ + const char *zKey = (const char*)sqlite3_column_text(pStmt, 1); + p->encryptionKey = sqlite3_mprintf("%s", zKey); + if( p->encryptionKey==0 ) rc = SQLITE_NOMEM; + }else + if( sqlite3_stricmp("encryptionType", zKey)==0 ){ + const char *zName = (const char*)sqlite3_column_text(pStmt, 1); + rc = zonefileEncryption(zName, &p->encryptionType, &zErr); + }else{ + rc = SQLITE_ERROR; + zErr = sqlite3_mprintf("unknown parameter name: \"%s\"", zKey); + } + } + rc2 = sqlite3_finalize(pStmt); + if( rc==SQLITE_OK ) rc = rc2; + + if( zErr ){ + sqlite3_result_error(pCtx, zErr, -1); + sqlite3_free(zErr); + }else if( rc ){ + sqlite3_result_error_code(pCtx, rc); + }else{ + if( p->encryptionKey==0 ){ + p->encryptionType = 0; + } + } + return rc; +} + +/* +** Check that there is room in buffer pBuf for at least nByte bytes more +** data. If not, attempt to allocate more space. If the allocation attempt +** fails, leave an error message in context pCtx and return SQLITE_ERROR. +** +** If no error occurs, SQLITE_OK is returned. +*/ +static int zonefileBufferGrow( + sqlite3_context *pCtx, + ZonefileBuffer *pBuf, + int nByte +){ + int nReq = pBuf->n + nByte; + if( nReq>pBuf->nAlloc ){ + u8 *aNew; + int nNew = pBuf->nAlloc ? pBuf->nAlloc*2 : 128; + while( nNewa, nNew); + if( aNew==0 ){ + sqlite3_result_error_nomem(pCtx); + return SQLITE_ERROR; + } + pBuf->a = aNew; + pBuf->nAlloc = nNew; + } + return SQLITE_OK; +} + +/* +** Free the memory allocation associated with buffer pBuf. +*/ +static void zonefileBufferFree(ZonefileBuffer *pBuf){ + sqlite3_free(pBuf->a); + memset(pBuf, 0, sizeof(ZonefileBuffer)); +} + +/* +** Read and return a 64-bit unsigned integer in big-endian format from +** buffer aBuf. +*/ +static u64 zonefileGet64(u8 *aBuf){ + return (((u64)aBuf[0]) << 56) + + (((u64)aBuf[1]) << 48) + + (((u64)aBuf[2]) << 40) + + (((u64)aBuf[3]) << 32) + + (((u64)aBuf[4]) << 24) + + (((u64)aBuf[5]) << 16) + + (((u64)aBuf[6]) << 8) + + (((u64)aBuf[7]) << 0); +} + +/* +** Append a 32-bit big-endian integer with value v to buffer pBuf. Space +** must have already been reserved in the buffer using zonefileBufferGrow(). +*/ +static void zonefileAppend32(ZonefileBuffer *pBuf, u32 v){ + zonefilePut32(&pBuf->a[pBuf->n], v); + pBuf->n += 4; +} + +/* +** Append a 64-bit big-endian integer with value v to buffer pBuf. Space +** must have already been reserved in the buffer using zonefileBufferGrow(). +*/ +static void zonefileAppend64(ZonefileBuffer *pBuf, u64 v){ + zonefileAppend32(pBuf, v>>32); + zonefileAppend32(pBuf, v & 0xFFFFFFFF); +} + +/* +** Append the n bytse of data in buffer p to buffer pBuf. Space must have +** already been reserved in the buffer using zonefileBufferGrow(). +*/ +static void zonefileAppendBlob(ZonefileBuffer *pBuf, const u8 *p, int n){ + memcpy(&pBuf->a[pBuf->n], p, n); + pBuf->n += n; +} + +/* +** Write nBuf bytes of data from buffer aBuf to the file opened by +** file-handle pFd. Return SQLITE_OK if successful, or SQLITE_ERROR +** otherwise. +*/ +static int zonefileFileWrite(FILE *pFd, const u8 *aBuf, int nBuf){ + size_t res = fwrite(aBuf, 1, nBuf, pFd); + return res!=(size_t)nBuf ? SQLITE_ERROR : SQLITE_OK; +} + +/* +** Read nBuf bytes of data from offset iOff of the file opened by +** file-handle pFd into buffer aBuf. Return SQLITE_OK if successful, or +** SQLITE_ERROR otherwise. +*/ +static int zonefileFileRead(FILE *pFd, u8 *aBuf, int nBuf, i64 iOff){ + int rc = fseek(pFd, (long)iOff, SEEK_SET); + if( rc==0 ){ + rc = fread(aBuf, 1, nBuf, pFd); + rc = (rc==nBuf) ? SQLITE_OK : SQLITE_ERROR; + }else{ + rc = SQLITE_ERROR; + } + return rc; +} + +/* +** Open the file identified by path zFile for writing (if bWrite==1) or +** reading (if bWrite==0). If successful, return a pointer to the new +** file-handle object. Otherwise, return NULL and set output variable +** (*pzErr) to point to a buffer containing an English language error +** message. It is the responsibility of the caller to eventually free +** any error message buffer using sqlite3_free(). +*/ +static FILE *zonefileFileOpen(const char *zFile, int bWrite, char **pzErr){ + FILE *pFd = fopen(zFile, bWrite ? "wb" : "rb"); + if( pFd==0 ){ + *pzErr = sqlite3_mprintf("failed to open file \"%s\" for %s", + zFile, bWrite ? "writing" : "reading" + ); + } + return pFd; +} + +/* +** Close the file handle passed as the only argument. +*/ +static void zonefileFileClose(FILE *pFd){ + if( pFd ) fclose(pFd); +} + +/* +** Append the contents of buffer pFrom to buffer pTo. If successful, return +** SQLITE_OK. Otherwise, return an SQLite error code and leave an error +** message in context object pCtx. +** +** If argument pMethod is not NULL, then it is used along with pCmp to +** compress the data before appending it to pFrom. Similarly, if argument +** pCodec is not NULL, then it is used to encrypt the data before it is +** appended. +*/ +static int zonefileAppendData( + sqlite3_context *pCtx, /* Leave any error message here */ + ZonefileCompress *pMethod, /* Compression method object */ + void *pCmp, /* Compression handle */ + ZonefileCodec *pCodec, /* Compression method, if any */ + ZonefileBuffer *pTo, /* Append new data here */ + ZonefileBuffer *pFrom /* Input buffer */ +){ + int rc = SQLITE_OK; + int nNonce = pCodec ? zonefileCodecNonceSize(pCodec) : 0; + int nOrig = pTo->n; + if( pMethod->eType==ZONEFILE_COMPRESSION_NONE ){ + if( zonefileBufferGrow(pCtx, pTo, pFrom->n + nNonce) ){ + rc = SQLITE_ERROR; + }else{ + zonefileAppendBlob(pTo, pFrom->a, pFrom->n); + } + }else{ + int nReq = pMethod->xCompressBound(pCmp, pFrom->n); + if( zonefileBufferGrow(pCtx, pTo, nReq + nNonce) ) return SQLITE_ERROR; + rc = pMethod->xCompress(pCmp, &pTo->a[pTo->n], &nReq, pFrom->a, pFrom->n); + pTo->n += nReq; + } + + /* Encrypt the data just appended to buffer pTo. */ + if( rc==SQLITE_OK && pCodec ){ + zonefileCodecEncode(pCodec, &pTo->a[nOrig], pTo->n - nOrig); + pTo->n += nNonce; + } + return rc; +} + +/* +** Append nByte bytes of garbage data to the file opened with pFd. Return +** SQLITE_OK if successful, or SQLITE_ERROR if an error occurs. +** +** Parameter nByte is only ever non-zero when running tests. So it doesn't +** matter if this function is inefficient in those cases. +*/ +static int zonefilePad(FILE *pFd, int nByte){ + assert( nByte>=0 && nByte<256 ); + if( nByte ){ + int nRem = nByte; + u8 buf[17] = "0123456789ABCDEF"; + while( nRem>0 ){ + int n = MIN(nRem, sizeof(buf)); + if( zonefileFileWrite(pFd, buf, n) ) return SQLITE_ERROR; + nRem -= n; + } + } + return SQLITE_OK; +} + +/* +** If character c is not a hexadecimal digit, return -1. Otherwise, return +** the value of the hex digit (a value between 0 and 15). +*/ +static int zonefileHexChar(char c){ + if( c>='0' && c<='9' ) return c-'0'; + c = c & ~0x20; + if( c>='A' && c<='F' ) return c-('A'-10); + return -1; +} + +/* +** String ZonefileParam.encryptionKey currently contains a string specified +** for the encryptionKey attribute of a JSON object passed to SQL function +** zonefile_write(). The string is (*pn) bytes in size. +** +** If the ZonefileParam.debugEncryptionKeyText flag is true this function +** is a no-op. Otherwise, an attempt is made to overwrite the hex string in +** ZonefileParam.encryptionKey with the corresponding binary data. If +** successful, SQLITE_OK is returned and (*pn) is set to the number of +** bytes in the binary key. Otherwise, if an error occurs, an SQLite error +** code is returned and (*pzErr) set to point to an English language error +** message. It is the responsibility of the caller to eventually free any +** error message buffer using sqlite3_free(). +*/ +static int zonefileDecodeEncryptionKey(ZonefileParam *p, int *pn, char **pzErr){ + if( p->debugEncryptionKeyText==0 ){ + u8 *z = (u8*)p->encryptionKey; + int n = *pn; + int i; + if( n&0x01 ) goto bad_format; + for(i=0; ixTrain ){ + zonefileCtxError(pCtx, + "compressor \"%s\" may not be used to compress the zonefile index", + sParam.pCmpIdx->zName + ); + goto zone_write_out; + } + + if( sParam.pCmpData->xOpen ){ + rc = sParam.pCmpData->xOpen(&pCmp, 0, 0); + if( rc!=SQLITE_OK ){ + zonefileCtxError(pCtx, "error in compressor construction"); + goto zone_write_out; + } + } + + /* Prepare the SQL statement used to read data from the source table. This + ** also serves to verify the suitability of the source table schema. */ + rc = zonefilePrepare(sqlite3_context_db_handle(pCtx), &pStmt, &zErr, + "SELECT k, frame, v FROM %Q ORDER BY frame, idx, k", zTbl + ); + + /* Open the file-handle used to write out the zonefile */ + if( rc==SQLITE_OK ){ + pFd = zonefileFileOpen(zFile, 1, &zErr); + } + if( pFd==0 ){ + sqlite3_result_error(pCtx, zErr, -1); + sqlite3_free(zErr); + goto zone_write_out; + } + + /* If the data compressor uses a global dictionary, create the dictionary + ** and store it in buffer sDict. */ + if( sParam.pCmpData->xTrain ){ + int nSample = 0; + + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + int nByte = sqlite3_column_bytes(pStmt, 2); + const u8 *aByte = (const u8*)sqlite3_column_blob(pStmt, 2); + if( zonefileBufferGrow(pCtx, &sSample, nByte) ){ + goto zone_write_out; + } + if( (nSample & (nSample-1))==0 ){ + int nNew = nSample ? nSample*2 : 8; + size_t *aNew = (size_t*)sqlite3_realloc(aSample, sizeof(size_t) * nNew); + if( aNew==0 ){ + sqlite3_result_error_nomem(pCtx); + goto zone_write_out; + } + aSample = aNew; + } + aSample[nSample] = nByte; + zonefileAppendBlob(&sSample, aByte, nByte); + nSample++; + } + rc = sqlite3_reset(pStmt); + if( rc!=SQLITE_OK ){ + zonefileTransferError(pCtx); + goto zone_write_out; + } + + if( zonefileBufferGrow(pCtx, &sDict, ZONEFILE_DEFAULT_DICTSIZE) ){ + goto zone_write_out; + } + sDict.n = sDict.nAlloc; + + rc = sParam.pCmpData->xTrain( + pCmp, sDict.a, &sDict.n, sSample.a, aSample, nSample + ); + if( rc!=SQLITE_OK ){ + zonefileCtxError(pCtx, "error generating dictionary"); + goto zone_write_out; + } + } + + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + sqlite3_int64 k = sqlite3_column_int64(pStmt, 0); + sqlite3_value *pFrame = sqlite3_column_value(pStmt, 1); + int nBlob = sqlite3_column_bytes(pStmt, 2); + const u8 *pBlob = (const u8*)sqlite3_column_blob(pStmt, 2); + + int bAuto = zonefileIsAutoFrame(pFrame); + if( sFrame.n>0 ){ + if( zonefileCompareValue(pFrame, pPrev) + || (bAuto && (sFrame.n+nBlob)>sParam.maxAutoFrameSize) + ){ + /* Add new entry to sFrame */ + if( zonefileBufferGrow(pCtx, &sFrameIdx, 4) + || zonefileAppendData(pCtx,sParam.pCmpData,pCmp,pCodec,&sData,&sFrame) + ){ + goto zone_write_out; + } + sFrame.n = 0; + zonefileAppend32(&sFrameIdx, sData.n); + sqlite3_value_free(pPrev); + pPrev = 0; + nFrame++; + } + } + + if( pPrev==0 ){ + pPrev = sqlite3_value_dup(pFrame); + if( pPrev==0 ){ + sqlite3_result_error_nomem(pCtx); + goto zone_write_out; + } + } + + /* Add new entry to sKeyIdx */ + if( zonefileBufferGrow(pCtx, &sKeyIdx, ZONEFILE_SZ_KEYOFFSETS_ENTRY) ){ + goto zone_write_out; + } + zonefileAppend64(&sKeyIdx, k); + zonefileAppend32(&sKeyIdx, nFrame); + zonefileAppend32(&sKeyIdx, sFrame.n); + zonefileAppend32(&sKeyIdx, nBlob); + + /* Add uncompressed data for new entry to sFrame */ + if( zonefileBufferGrow(pCtx, &sFrame, nBlob) ) goto zone_write_out; + zonefileAppendBlob(&sFrame, pBlob, nBlob); + nKey++; + } + + if( sFrame.n>0 ){ + if( zonefileBufferGrow(pCtx, &sFrameIdx, 4) + || zonefileAppendData(pCtx, sParam.pCmpData, pCmp, pCodec, &sData, &sFrame) + ){ + goto zone_write_out; + } + zonefileAppend32(&sFrameIdx, sData.n); + nFrame++; + } + + /* If a compression method was specified, compress the key-index here */ + if( sParam.pCmpIdx->eType!=ZONEFILE_COMPRESSION_NONE ){ + if( zonefileBufferGrow(pCtx, &sFrameIdx, sKeyIdx.n) ) goto zone_write_out; + zonefileAppendBlob(&sFrameIdx, sKeyIdx.a, sKeyIdx.n); + zonefileBufferFree(&sKeyIdx); + rc = zonefileAppendData(pCtx, sParam.pCmpIdx, 0, 0, &sKeyIdx, &sFrameIdx); + sFrameIdx.n = 0; + if( rc ) goto zone_write_out; + } + + /* Create the zonefile header in the in-memory buffer */ + memset(aHdr, 0, ZONEFILE_SZ_HEADER); + zonefilePut32(&aHdr[0], ZONEFILE_MAGIC_NUMBER); + aHdr[4] = (u8)sParam.pCmpIdx->eType; + aHdr[5] = (u8)sParam.pCmpData->eType; + iOff = ZONEFILE_SZ_HEADER + sFrameIdx.n + sKeyIdx.n; + zonefilePut32(&aHdr[6], sDict.n ? iOff+sParam.debugExtendedHeaderSize : 0); + zonefilePut32(&aHdr[10], iOff + sParam.debugExtendedHeaderSize + sDict.n); + zonefilePut32(&aHdr[14], nFrame); + zonefilePut32(&aHdr[18], nKey); + aHdr[22] = (u8)sParam.encryptionType; + aHdr[23] = 0; /* Encryption key index */ + aHdr[24] = 0; /* extended header version */ + aHdr[25] = (u8)sParam.debugExtendedHeaderSize; + assert( ZONEFILE_SZ_HEADER>=26 ); + + rc = zonefileFileWrite(pFd, aHdr, ZONEFILE_SZ_HEADER); + if( rc==SQLITE_OK ) rc = zonefilePad(pFd, sParam.debugExtendedHeaderSize); + if( rc==SQLITE_OK ) rc = zonefileFileWrite(pFd, sFrameIdx.a, sFrameIdx.n); + if( rc==SQLITE_OK ) rc = zonefileFileWrite(pFd, sKeyIdx.a, sKeyIdx.n); + if( rc==SQLITE_OK ) rc = zonefileFileWrite(pFd, sDict.a, sDict.n); + if( rc==SQLITE_OK ) rc = zonefileFileWrite(pFd, sData.a, sData.n); + if( rc ){ + zonefileCtxError(pCtx, "error writing file \"%s\" (fwrite())", zFile); + goto zone_write_out; + } + + if( fclose(pFd) ){ + zonefileCtxError(pCtx, "error writing file \"%s\" (fclose())", zFile); + } + pFd = 0; + + zone_write_out: + if( pCmp ) sParam.pCmpData->xClose(pCmp); + if( pFd ) fclose(pFd); + sqlite3_value_free(pPrev); + sqlite3_finalize(pStmt); + zonefileCodecDestroy(pCodec); + zonefileBufferFree(&sFrameIdx); + zonefileBufferFree(&sKeyIdx); + zonefileBufferFree(&sFrame); + zonefileBufferFree(&sDict); + zonefileBufferFree(&sData); + zonefileBufferFree(&sSample); + sqlite3_free(aSample); + sqlite3_free(sParam.encryptionKey); + if( rc==SQLITE_NOMEM ){ + sqlite3_result_error_nomem(pCtx); + } +} + +/* +** Virtual table type for zonefile_files virtual tables. +*/ +typedef struct ZonefileFilesTab ZonefileFilesTab; +struct ZonefileFilesTab { + sqlite3_vtab base; /* Base class - must be first */ + sqlite3 *db; + char *zBase; /* Name of this table */ + char *zDb; /* Database containing this table */ + ZonefileGlobal *pGlobal; /* Global object */ + sqlite3_stmt *pInsert; /* Insert into the %_shadow_file table */ + sqlite3_stmt *pInsertIdx; /* Insert into the %_shadow_idx table */ + sqlite3_stmt *pDeleteIdx; /* Delete by fileid from %_shadow_idx table */ + sqlite3_stmt *pDelete; /* Delete by rowid from %_shadow_file table */ +}; + +/* +** Virtual table cursor type for zonefile_files virtual tables. +*/ +typedef struct ZonefileFilesCsr ZonefileFilesCsr; +struct ZonefileFilesCsr { + sqlite3_vtab_cursor base; /* Base class - must be first */ + sqlite3_stmt *pSelect; +}; + +/* +** This function does the work of xCreate (if bCreate!=0) or xConnect +** (if bCreate==0) for the zonefile_files module. +*/ +static int zffCreateConnect( + int bCreate, + void *pAux, + sqlite3 *db, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + ZonefileFilesTab *p; + const char *zName = argv[2]; + const char *zDb = argv[1]; + int nName = strlen(zName); + int nDb = strlen(zDb); + int rc = SQLITE_OK; + + if( nName<6 || memcmp(&zName[nName-6], "_files", 6)!=0 ){ + *pzErr = sqlite3_mprintf("do not create zonefile_files tables directly!"); + *ppVtab = 0; + return SQLITE_ERROR; + } + + p = (ZonefileFilesTab*)sqlite3_malloc(sizeof(ZonefileFilesTab)+nName+1+nDb+1); + if( !p ){ + rc = SQLITE_NOMEM; + }else{ + memset(p, 0, sizeof(ZonefileFilesTab)); + p->zBase = (char*)&p[1]; + memcpy(p->zBase, zName, nName-6); + p->zBase[nName-6] = '\0'; + p->zDb = &p->zBase[nName+1]; + memcpy(p->zDb, zDb, nDb+1); + p->db = db; + p->pGlobal = (ZonefileGlobal*)pAux; + rc = sqlite3_declare_vtab(db, ZONEFILE_FILES_SCHEMA); + } + + if( rc!=SQLITE_OK ){ + sqlite3_free(p); + p = 0; + } + *ppVtab = (sqlite3_vtab*)p; + return rc; +} + +/* +** zonefile_files virtual table module xCreate method. +*/ +static int zffCreate( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + return zffCreateConnect(1, pAux, db, argc, argv, ppVtab, pzErr); +} + +/* +** zonefile_files virtual table module xConnect method. +*/ +static int zffConnect( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + return zffCreateConnect(0, pAux, db, argc, argv, ppVtab, pzErr); +} + +/* +** zonefile_files virtual table module xDisconnect method. +*/ +static int zffDisconnect(sqlite3_vtab *pVtab){ + ZonefileFilesTab *pTab = (ZonefileFilesTab*)pVtab; + sqlite3_finalize(pTab->pInsert); + sqlite3_finalize(pTab->pDelete); + sqlite3_finalize(pTab->pInsertIdx); + sqlite3_finalize(pTab->pDeleteIdx); + sqlite3_free(pTab); + return SQLITE_OK; +} + +/* +** zonefile_files virtual table module xBestIndex method. +*/ +static int zffBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pInfo){ + return SQLITE_OK; +} + +/* +** zonefile_files virtual table module xOpen method. +*/ +static int zffOpen(sqlite3_vtab *pVtab, sqlite3_vtab_cursor **ppCursor){ + ZonefileFilesCsr *pCsr; + pCsr = (ZonefileFilesCsr*)sqlite3_malloc(sizeof(ZonefileFilesCsr)); + if( pCsr==0 ){ + return SQLITE_NOMEM; + } + memset(pCsr, 0, sizeof(ZonefileFilesCsr)); + *ppCursor = (sqlite3_vtab_cursor*)pCsr; + return SQLITE_OK; +} + +/* +** Reset a ZonefileFilesCsr object to the state it is in immediately after +** it is allocated by zffOpen(). +*/ +static void zffCursorReset(ZonefileFilesCsr *pCsr){ + sqlite3_finalize(pCsr->pSelect); + pCsr->pSelect = 0; +} + +/* +** zonefile_files virtual table module xClose method. +*/ +static int zffClose(sqlite3_vtab_cursor *cur){ + ZonefileFilesCsr *pCsr = (ZonefileFilesCsr*)cur; + zffCursorReset(pCsr); + sqlite3_free(pCsr); + return SQLITE_OK; +} + +/* +** zonefile_files virtual table module xNext method. +*/ +static int zffNext(sqlite3_vtab_cursor *cur){ + ZonefileFilesCsr *pCsr = (ZonefileFilesCsr*)cur; + int rc; + if( SQLITE_ROW==sqlite3_step(pCsr->pSelect) ){ + rc = SQLITE_OK; + }else{ + rc = sqlite3_finalize(pCsr->pSelect); + pCsr->pSelect = 0; + } + return rc; +} + +/* +** zonefile_files virtual table module xFilter method. +*/ +static int zffFilter( + sqlite3_vtab_cursor *cur, + int idxNum, const char *idxStr, + int argc, sqlite3_value **argv +){ + ZonefileFilesCsr *pCsr = (ZonefileFilesCsr*)cur; + ZonefileFilesTab *pTab = (ZonefileFilesTab*)(pCsr->base.pVtab); + int rc; + zffCursorReset(pCsr); + + rc = zonefilePrepare(pTab->db, &pCsr->pSelect, &pTab->base.zErrMsg, + "SELECT filename, fileid FROM %Q.'%q_shadow_file'", + pTab->zDb, pTab->zBase + ); + if( rc==SQLITE_OK ){ + rc = zffNext(cur); + } + return rc; +} + +/* +** zonefile_files virtual table module xEof method. +*/ +static int zffEof(sqlite3_vtab_cursor *cur){ + ZonefileFilesCsr *pCsr = (ZonefileFilesCsr*)cur; + return pCsr->pSelect==0; +} + +/* +** Deserialize the ZONEFILE_SZ_HEADER byte zonefile header in the +** buffer. Populate (*pHdr) with the results. +*/ +static void zonefileHeaderDeserialize(u8 *aBuf, ZonefileHeader *pHdr){ + pHdr->magicNumber = zonefileGet32(&aBuf[0]); + pHdr->compressionTypeIndexData = aBuf[4]; + pHdr->compressionTypeContent = aBuf[5]; + pHdr->byteOffsetDictionary = zonefileGet32(&aBuf[6]); + pHdr->byteOffsetFrames = zonefileGet32(&aBuf[10]); + pHdr->numFrames = zonefileGet32(&aBuf[14]); + pHdr->numKeys = zonefileGet32(&aBuf[18]); + pHdr->encryptionType = aBuf[22]; + pHdr->encryptionKeyIdx = aBuf[23]; + pHdr->extendedHeaderVersion = aBuf[24]; + pHdr->extendedHeaderSize = aBuf[25]; +} + +/* +** Read and decode a Zonefile header from the start of the file opened +** by file-handle pFd. If successful, populate object (*pHdr) before +** returning SQLITE_OK. Otherwise, if an error occurs, set output +** parameter (*pzErr) to point to an English language error message and +** return an SQLite error code. +** +** It is the responsibility of the caller to eventually free any error +** message returned via (*pzErr) using sqlite3_free(). +*/ +static int zonefileReadHeader( + FILE *pFd, /* File to read from */ + const char *zFile, /* Name of file opened by pFd */ + ZonefileHeader *pHdr, /* Populate this object before returning */ + char **pzErr /* OUT: Error message */ +){ + u8 aBuf[ZONEFILE_SZ_HEADER]; + int rc = zonefileFileRead(pFd, aBuf, ZONEFILE_SZ_HEADER, 0); + if( rc==SQLITE_OK ){ + zonefileHeaderDeserialize(aBuf, pHdr); + if( pHdr->magicNumber!=ZONEFILE_MAGIC_NUMBER ){ + rc = SQLITE_ERROR; + } + } + + if( rc!=SQLITE_OK ){ + *pzErr = sqlite3_mprintf( + "failed to read zonefile header from file \"%s\"", zFile + ); + } + + return rc; +} + +/* +** Read the zonefile header from file zFile and set the result of pCtx +** to a JSON object that represents the contents. Or, if an error occurs, +** leave an error message in pCtx. This function is called whenever the +** "header" column of a zonefile_files virtual table is requested. +*/ +static void zonefileJsonHeader(sqlite3_context *pCtx, const char *zFile){ + char *zErr = 0; + int rc = SQLITE_OK; + ZonefileHeader hdr = {0}; + + FILE *pFd = zonefileFileOpen(zFile, 0, &zErr); + if( pFd ){ + rc = zonefileReadHeader(pFd, zFile, &hdr, &zErr); + }else{ + rc = SQLITE_ERROR; + } + + if( rc==SQLITE_OK ){ + char *zJson = sqlite3_mprintf("{" + "\"magicNumber\":%u," + "\"compressionTypeIndexData\":%u," + "\"compressionTypeContent\":%u," + "\"byteOffsetDictionary\":%u," + "\"byteOffsetFrames\":%u," + "\"numFrames\":%u," + "\"numKeys\":%u," + "\"encryptionType\":%u," + "\"encryptionKeyIdx\":%u," + "\"extendedHeaderVersion\":%u," + "\"extendedHeaderSize\":%u}", + (u32)hdr.magicNumber, + (u32)hdr.compressionTypeIndexData, + (u32)hdr.compressionTypeContent, + (u32)hdr.byteOffsetDictionary, + (u32)hdr.byteOffsetFrames, + (u32)hdr.numFrames, + (u32)hdr.numKeys, + (u32)hdr.encryptionType, + (u32)hdr.encryptionKeyIdx, + (u32)hdr.extendedHeaderVersion, + (u32)hdr.extendedHeaderSize + ); + if( zJson ){ + sqlite3_result_text(pCtx, zJson, -1, SQLITE_TRANSIENT); + sqlite3_free(zJson); + }else{ + sqlite3_result_error_nomem(pCtx); + } + }else{ + sqlite3_result_error(pCtx, zErr, -1); + sqlite3_free(zErr); + } + + zonefileFileClose(pFd); +} + +/* +** zonefile_files virtual table module xColumn method. +*/ +static int zffColumn(sqlite3_vtab_cursor *cur, sqlite3_context *ctx, int i){ + ZonefileFilesCsr *pCsr = (ZonefileFilesCsr*)cur; + if( sqlite3_vtab_nochange(ctx) ){ + return SQLITE_OK; + } + switch( i ){ + case 0: /* filename */ + sqlite3_result_value(ctx, sqlite3_column_value(pCsr->pSelect, 0)); + break; + case 1: /* ekey */ + break; + default: { + const char *zFile = (const char*)sqlite3_column_text(pCsr->pSelect, 0); + zonefileJsonHeader(ctx, zFile); + assert( i==2 ); + break; + } + } + return SQLITE_OK; +} + +/* +** zonefile_files virtual table module xRowid method. +*/ +static int zffRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ + ZonefileFilesCsr *pCsr = (ZonefileFilesCsr*)cur; + *pRowid = sqlite3_column_int64(pCsr->pSelect, 1); + return SQLITE_OK; +} + +/* +** Uncompress buffer aIn/nIn using the compression routines pMethod +** and the compressor instance pCmp into space obtained from +** sqlite3_malloc(). If successful, return SQLITE_OK and set output +** parameters (*paOut) and (*pnOut) to point to the allocated buffer +** and its size in bytes respectively. In this case it is the +** responsibility of the caller to eventually free buffer (*paOut) +** using sqlite3_free(). +** +** Or, if an error occurs, set both output parameters to zero and +** return an SQLite error code. +*/ +static int zonefileUncompress( + ZonefileCompress *pMethod, /* Compression routines */ + void *pCmp, /* Compressor instance */ + u8 *aIn, int nIn, /* Input buffer */ + u8 **paOut, int *pnOut /* Output buffer */ +){ + int rc; + int nOut = pMethod->xUncompressSize(pCmp, aIn, nIn); + u8 *aOut = sqlite3_malloc(nOut); + + assert( pMethod->eType!=ZONEFILE_COMPRESSION_NONE ); + if( aOut==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = pMethod->xUncompress(pCmp, aOut, nOut, aIn, nIn); + } + if( rc ){ + sqlite3_free(aOut); + aOut = 0; + } + + *paOut = aOut; + *pnOut = nOut; + return rc; +} + +/* +** Attempt to find the compression methods object for the compression method +** identified by integer constant eType (defined as part of the zonefile file +** format). If successful, set output parameter (*pp) to point to the object +** and return SQLITE_OK. Otherwise, return an SQLite error code and set +** (*pzErr) to point to a buffer containing an English language error +** message. It is the responsibility of the caller to eventually free any +** error message buffer using sqlite3_free(). +*/ +static int zonefileFindCompress(int eType, ZonefileCompress **pp, char **pzErr){ + int rc = SQLITE_OK; + ZonefileCompress *pCmp; + pCmp = zonefileCompressByValue(eType); + if( pCmp==0 ){ + *pzErr = sqlite3_mprintf("unsupported compression method: %d", eType); + rc = SQLITE_ERROR; + }else if( pCmp->eType==ZONEFILE_COMPRESSION_NONE ){ + pCmp = 0; + } + *pp = pCmp; + return rc; +} + +/* +** Argument pHdr points to a deserialized zonefile header read from the +** zonefile opened by file handle pFd. This function attempts to read +** the entire zonefile-index structure into space obtained from +** sqlite3_malloc(). If successful, it sets output parameters (*paIdx) +** and (*pnIdx) to point to the buffer and its size in bytes respectively +** before returning SQLITE_OK. +** +** Otherwise, if an error occurs, an SQLite error code is returned and +** output parameter (*pzErr) may be set to point to a buffer containing an +** English language error message. It is the responsibility of the caller to +** eventually free any error message buffer using sqlite3_free(). +*/ +static int zonefileLoadIndex( + ZonefileHeader *pHdr, /* Deserialized header read from file */ + FILE *pFd, /* File to read from */ + u8 **paIdx, int *pnIdx, /* OUT: Buffer containing zonefile index */ + char **pzErr /* OUT: Error message */ +){ + ZonefileCompress *pCmp = 0; + int rc; + u8 *aIdx = 0; + int nIdx = 0; + + rc = zonefileFindCompress(pHdr->compressionTypeIndexData, &pCmp, pzErr); + if( rc==SQLITE_OK ){ + if( pHdr->byteOffsetDictionary ){ + nIdx = pHdr->byteOffsetDictionary - ZONEFILE_SZ_HEADER; + }else{ + nIdx = pHdr->byteOffsetFrames - ZONEFILE_SZ_HEADER; + } + nIdx -= pHdr->extendedHeaderSize; + aIdx = (u8*)sqlite3_malloc(nIdx); + + if( aIdx==0 ){ + rc = SQLITE_NOMEM; + }else{ + i64 iOff = ZONEFILE_SZ_HEADER + pHdr->extendedHeaderSize; + rc = zonefileFileRead(pFd, aIdx, nIdx, iOff); + } + } + + if( rc==SQLITE_OK && pCmp ){ + u8 *aUn = 0; + int nUn = 0; + rc = zonefileUncompress(pCmp, 0, aIdx, nIdx, &aUn, &nUn); + if( rc==SQLITE_ERROR ){ + *pzErr = sqlite3_mprintf("failed to uncompress index"); + } + sqlite3_free(aIdx); + aIdx = aUn; + nIdx = nUn; + } + + if( rc!=SQLITE_OK ){ + sqlite3_free(aIdx); + aIdx = 0; + nIdx = 0; + } + + *paIdx = aIdx; + *pnIdx = nIdx; + return rc; +} + + +static int zonefilePopulateIndex( + ZonefileFilesTab *pTab, + const char *zFile, + i64 iFileid +){ + ZonefileHeader hdr = { 0 }; + int rc; + FILE *pFd = zonefileFileOpen(zFile, 0, &pTab->base.zErrMsg); + + if( pFd==0 ){ + rc = SQLITE_ERROR; + }else{ + rc = zonefileReadHeader(pFd, zFile, &hdr, &pTab->base.zErrMsg); + } + + if( rc==SQLITE_OK && hdr.numKeys>0 ){ + u8 *aKey; /* Entire KeyOffsets array */ + int nKey; /* Size of buffer aKey[] in bytes */ + int i; + + rc = zonefileLoadIndex(&hdr, pFd, &aKey, &nKey, &pTab->base.zErrMsg); + + if( rc==SQLITE_OK && pTab->pInsertIdx==0 ){ + rc = zonefilePrepare(pTab->db, &pTab->pInsertIdx, &pTab->base.zErrMsg, + "INSERT INTO %Q.'%q_shadow_idx'(k, fileid, fofst, fsz, ofst, sz)" + "VALUES(?,?,?,?,?,?)", + pTab->zDb, pTab->zBase + ); + } + + for(i=0; (u32)i0 ){ + iFrameOff = zonefileGet32(&aKey[(iFrame-1)*4]); + szFrame -= (int)iFrameOff; + } + iFrameOff += hdr.byteOffsetFrames; + iOff = (i64)zonefileGet32(&aEntry[12]); + sz = (int)zonefileGet32(&aEntry[16]); + + sqlite3_bind_int64(pTab->pInsertIdx, 1, (i64)zonefileGet64(&aEntry[0])); + sqlite3_bind_int64(pTab->pInsertIdx, 2, iFileid); + if( hdr.encryptionType || hdr.compressionTypeContent ){ + sqlite3_bind_int64(pTab->pInsertIdx, 5, iOff); + sqlite3_bind_int(pTab->pInsertIdx, 6, sz); + }else{ + iFrameOff += iOff; + szFrame = sz; + } + sqlite3_bind_int64(pTab->pInsertIdx, 3, iFrameOff); + sqlite3_bind_int(pTab->pInsertIdx, 4, szFrame); + + sqlite3_step(pTab->pInsertIdx); + rc = sqlite3_reset(pTab->pInsertIdx); + if( rc!=SQLITE_OK ){ + pTab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(pTab->db)); + } + } + sqlite3_free(aKey); + } + + zonefileFileClose(pFd); + return rc; +} + +/* +** zonefile_files virtual table module xUpdate method. +** +** A delete specifies a single argument - the rowid of the row to remove. +** +** Update and insert operations pass: +** +** 1. The "old" rowid, or NULL. +** 2. The "new" rowid. +** 3. Values for each of the 3 columns: (filename,ekey,header) +*/ +static int zffUpdate( + sqlite3_vtab *pVtab, + int nVal, + sqlite3_value **apVal, + sqlite_int64 *pRowid +){ + int rc = SQLITE_OK; + int bUpdateKey = 0; + ZonefileFilesTab *pTab = (ZonefileFilesTab*)pVtab; + + if( sqlite3_value_type(apVal[0])==SQLITE_INTEGER ){ + if( nVal>1 && sqlite3_value_nochange(apVal[2]) ){ + bUpdateKey = 1; + }else{ + if( pTab->pDelete==0 ){ + rc = zonefilePrepare(pTab->db, &pTab->pDelete, &pVtab->zErrMsg, + "DELETE FROM %Q.'%q_shadow_file' WHERE fileid=?", + pTab->zDb, pTab->zBase + ); + } + if( rc==SQLITE_OK && pTab->pDeleteIdx==0 ){ + rc = zonefilePrepare(pTab->db, &pTab->pDeleteIdx, &pVtab->zErrMsg, + "DELETE FROM %Q.'%q_shadow_idx' WHERE fileid=?", + pTab->zDb, pTab->zBase + ); + } + + if( rc==SQLITE_OK ){ + sqlite3_bind_value(pTab->pDelete, 1, apVal[0]); + sqlite3_step(pTab->pDelete); + rc = sqlite3_reset(pTab->pDelete); + } + if( rc==SQLITE_OK ){ + sqlite3_bind_value(pTab->pDeleteIdx, 1, apVal[0]); + sqlite3_step(pTab->pDeleteIdx); + rc = sqlite3_reset(pTab->pDeleteIdx); + } + } + } + if( nVal>1 ){ + i64 iFileid = 0; + if( bUpdateKey ){ + iFileid = sqlite3_value_int64(apVal[0]); + }else{ + const char *zFile = (const char*)sqlite3_value_text(apVal[2]); + + if( pTab->pInsert==0 ){ + rc = zonefilePrepare(pTab->db, &pTab->pInsert, &pVtab->zErrMsg, + "INSERT INTO %Q.'%q_shadow_file'(filename) VALUES(?)", + pTab->zDb, pTab->zBase + ); + } + + /* Add the new entry to the %_shadow_file table. */ + if( rc==SQLITE_OK ){ + sqlite3_bind_text(pTab->pInsert, 1, zFile, -1, SQLITE_TRANSIENT); + sqlite3_step(pTab->pInsert); + rc = sqlite3_reset(pTab->pInsert); + } + + /* Populate the %_shadow_idx table with entries for all keys in + ** the zonefile just added to %_shadow_file. */ + if( rc==SQLITE_OK ){ + iFileid = sqlite3_last_insert_rowid(pTab->db); + rc = zonefilePopulateIndex(pTab, zFile, iFileid); + } + } + + if( rc==SQLITE_OK ){ + int nKey = sqlite3_value_bytes(apVal[3]); + const u8 *aKey = (const u8*)sqlite3_value_blob(apVal[3]); + rc = zonefileKeyStore( + pTab->pGlobal, pTab->zDb, pTab->zBase, iFileid, aKey, nKey + ); + } + } + + return rc; +} + +/* Each entry in the frame-cache is represented by an instance of the +** following structure. */ +typedef struct ZonefileFrame ZonefileFrame; +struct ZonefileFrame { + i64 iFileid; /* Fileid for this frame */ + i64 iFrameOff; /* Offset of frame in file */ + int nBuf; /* Size of aBuf[] in bytes */ + u8 *aBuf; /* Buffer containing uncompressed frame data */ + ZonefileFrame *pLruNext; /* Next element in LRU list */ + ZonefileFrame *pLruPrev; /* Previous element in LRU list */ + ZonefileFrame *pHashNext; /* Next element in same hash bucket */ +}; + +typedef struct ZonefileTab ZonefileTab; +struct ZonefileTab { + sqlite3_vtab base; /* Base class - must be first */ + sqlite3 *db; + sqlite3_stmt *pIdToName; /* Translate fileid to filename */ + ZonefileGlobal *pGlobal; + char *zName; /* Name of this table */ + char *zDb; /* Name of db containing this table */ + + /* The following variables are used to implement the frame-cache */ + int nCacheSize; /* Configured cache size */ + int nCacheEntry; /* Current number of entries in cache */ + int nCacheBucket; /* Number of buckets in frame cache hash table */ + ZonefileFrame **aCache; /* Array of hash buckets */ + ZonefileFrame *pCacheFirst;/* Most recently used frame */ + ZonefileFrame *pCacheLast; /* Least recently used frame (first to discard) */ +}; + +typedef struct ZonefileCsr ZonefileCsr; +struct ZonefileCsr { + sqlite3_vtab_cursor base; /* Base class - must be first */ + sqlite3_stmt *pSelect; /* SELECT on %_shadow_idx table */ +}; + +/* +** Attempt to interpret the contents of string z as an integer. If +** successful, set (*piVal) to the integer value and return SQLITE_OK. +** Otherwise, return SQLITE_ERROR. +*/ +static int zonefileParseInteger(const char *z, int *piVal){ + *piVal = atoi(z); + return SQLITE_OK; +} + +/* +** Return true character i is considered to be whitespace. +*/ +static int zonefile_isspace(char i){ + return (i==' '); +} + +/* +** This function is called as part of constructing zonefile virtual table +** pTab. Argument zOption is the full text of a parameter (column name) +** specified as part of the CREATE VIRTUAL TABLE statement. This function +** attempts to interpret the parameter and update structure pTab +** accordingly. If successful, SQLITE_OK is returned. Otherwise, an +** SQLite error code is returned and (*pzErr) is left pointing to +** a buffer containing an English language error message. It is the +** responsibility of the caller to eventually free this buffer using +** sqlite3_free(). +*/ +static int zonefileParseOption( + ZonefileTab *pTab, /* Zonefile vtab under construction */ + const char *zOption, /* Text of option (column name) */ + char **pzErr /* OUT: Error message */ +){ + const char *z = zOption; + const char *zOpt = z; + int nOpt; + const char *zVal; + int rc = SQLITE_OK; + + /* Skip until EOF, whitespace or "=" */ + assert( 0==zonefile_isspace(*z) ); + while( *z && !zonefile_isspace(*z) && *z!='=' ) z++; + nOpt = z-zOpt; + + /* Skip whitespace. Then check there is an "=". */ + while( zonefile_isspace(*z) ) z++; + if( *z!='=' ) goto parse_error; + z++; + while( zonefile_isspace(*z) ) z++; + zVal = z; + + if( nOpt==9 && sqlite3_strnicmp(zOpt, "cachesize", 9)==0 ){ + rc = zonefileParseInteger(zVal, &pTab->nCacheSize); + }else{ + goto parse_error; + } + + return rc; + + parse_error: + *pzErr = sqlite3_mprintf("parse error in option: %s", zOption); + return SQLITE_ERROR; +} + +/* +** This function does the work of xCreate (if bCreate!=0) or xConnect +** (if bCreate==0) for the zonefile module. +** +** argv[0] -> module name ("zonefile") +** argv[1] -> database name +** argv[2] -> table name +*/ +static int zonefileCreateConnect( + int bCreate, + void *pAux, + sqlite3 *db, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + ZonefileTab *p; + const char *zName = argv[2]; + const char *zDb = argv[1]; + int nName = strlen(zName); + int nDb = strlen(zDb); + int rc = SQLITE_OK; + + p = (ZonefileTab*)sqlite3_malloc(sizeof(ZonefileTab) + nName+1 + nDb+1); + if( !p ){ + rc = SQLITE_NOMEM; + }else{ + int i; + memset(p, 0, sizeof(ZonefileTab)); + p->zName = (char*)&p[1]; + memcpy(p->zName, zName, nName+1); + p->zDb = &p->zName[nName+1]; + memcpy(p->zDb, zDb, nDb+1); + p->db = db; + p->pGlobal = (ZonefileGlobal*)pAux; + + if( bCreate ){ + char *zSql = sqlite3_mprintf( + "CREATE TABLE %Q.'%q_shadow_idx'(" + " k INTEGER PRIMARY KEY," + " fileid INTEGER," + " fofst INTEGER," + " fsz INTEGER," + " ofst INTEGER," + " sz INTEGER" + ");" + "CREATE TABLE %Q.'%q_shadow_file'(" + " filename TEXT," + " fileid INTEGER PRIMARY KEY" + ");" + "CREATE VIRTUAL TABLE %Q.'%q_files' USING zonefile_files;", + zDb, zName, zDb, zName, zDb, zName + ); + + if( zSql==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_exec(db, zSql, 0, 0, pzErr); + sqlite3_free(zSql); + } + } + + if( rc==SQLITE_OK ){ + rc = sqlite3_declare_vtab(db, ZONEFILE_SCHEMA); + } + + for(i=3; inCacheSize<1 ) p->nCacheSize = 1; + } + + if( rc!=SQLITE_OK ){ + sqlite3_free(p); + p = 0; + } + *ppVtab = (sqlite3_vtab*)p; + return rc; +} + +/* +** zonefile virtual table module xCreate method. +*/ +static int zonefileCreate( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + return zonefileCreateConnect(1, pAux, db, argc, argv, ppVtab, pzErr); +} + +/* +** zonefile virtual table module xConnect method. +*/ +static int zonefileConnect( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + return zonefileCreateConnect(0, pAux, db, argc, argv, ppVtab, pzErr); +} + +/* +** Zonefile virtual table module xBestIndex method. +** +** Equality and range constraints on either the rowid or column "k" (which +** are the same thing) are processed. Bits in the idxNum parameter are +** set to indicate the constraints present: +** +** 0x01: k == ? +** 0x02: k < ? +** 0x04: k <= ? +** 0x08: k > ? +** 0x10: k >= ? +** +** Only some combinations are valid: +** +** * If an == constraint is found, no other bits are set. +** * If a < constraint is present, any <= is ignored. +** * If a > constraint is present, any >= is ignored. +*/ +static int zonefileBestIndex(sqlite3_vtab *pVtab, sqlite3_index_info *pInfo){ + int iEq = -1; + int iLt = -1; + int iLe = -1; + int iGt = -1; + int iGe = -1; + int i; + int idxNum = 0; + double cost = 1000000000.0; + + for(i=0; inConstraint; i++){ + struct sqlite3_index_constraint *p = &pInfo->aConstraint[i]; + if( p->usable && p->iColumn<=0 ){ + switch( p->op ){ + case SQLITE_INDEX_CONSTRAINT_EQ: iEq = i; break; + case SQLITE_INDEX_CONSTRAINT_LT: iLt = i; break; + case SQLITE_INDEX_CONSTRAINT_LE: iLe = i; break; + case SQLITE_INDEX_CONSTRAINT_GT: iGt = i; break; + case SQLITE_INDEX_CONSTRAINT_GE: iGe = i; break; + } + } + } + + if( iEq>=0 ){ + cost = 10.0; + idxNum = 0x01; + pInfo->aConstraintUsage[iEq].argvIndex = 1; + }else{ + int iIdx = 1; + if( iLt>=0 ){ + pInfo->aConstraintUsage[iLt].argvIndex = iIdx++; + idxNum |= 0x02; + }else if( iLe>=0 ){ + pInfo->aConstraintUsage[iLe].argvIndex = iIdx++; + idxNum |= 0x04; + } + if( iGt>=0 ){ + pInfo->aConstraintUsage[iGt].argvIndex = iIdx++; + idxNum |= 0x08; + }else if( iGe>=0 ){ + pInfo->aConstraintUsage[iGe].argvIndex = iIdx++; + idxNum |= 0x10; + } + + if( iIdx==2 ) cost = 10000.0; + if( iIdx==3 ) cost = 100.0; + } + + pInfo->idxNum = idxNum; + pInfo->estimatedCost = cost; + + return SQLITE_OK; +} + +/* +** zonefile virtual table module xOpen method. +*/ +static int zonefileOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){ + ZonefileCsr *pCsr; + pCsr = (ZonefileCsr*)sqlite3_malloc(sizeof(ZonefileCsr)); + if( pCsr==0 ){ + return SQLITE_NOMEM; + } + memset(pCsr, 0, sizeof(ZonefileCsr)); + *ppCursor = (sqlite3_vtab_cursor*)pCsr; + return SQLITE_OK; +} + +/* +** Reset a ZonefileCsr object to the state it is in immediately after +** it is allocated by zffOpen(). +*/ +static void zonefileCursorReset(ZonefileCsr *pCsr){ + sqlite3_finalize(pCsr->pSelect); + pCsr->pSelect = 0; +} + +/* +** zonefile virtual table module xClose method. +*/ +static int zonefileClose(sqlite3_vtab_cursor *cur){ + ZonefileCsr *pCsr = (ZonefileCsr*)cur; + zonefileCursorReset(pCsr); + sqlite3_free(pCsr); + return SQLITE_OK; +} + +/* +** zonefile virtual table module xNext method. +*/ +static int zonefileNext(sqlite3_vtab_cursor *cur){ + ZonefileCsr *pCsr = (ZonefileCsr*)cur; + int rc = SQLITE_OK; + if( SQLITE_ROW!=sqlite3_step(pCsr->pSelect) ){ + rc = sqlite3_finalize(pCsr->pSelect); + if( rc!=SQLITE_OK ){ + ZonefileTab *pTab = (ZonefileTab*)pCsr->base.pVtab; + pTab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(pTab->db)); + } + pCsr->pSelect = 0; + } + return rc; +} + +/* +** zonefile virtual table module xFilter method. +*/ +static int zonefileFilter( + sqlite3_vtab_cursor *cur, + int idxNum, const char *idxStr, + int argc, sqlite3_value **argv +){ + ZonefileCsr *pCsr = (ZonefileCsr*)cur; + ZonefileTab *pTab = (ZonefileTab*)pCsr->base.pVtab; + int rc; + + const char *z1 = 0; + const char *z2 = 0; + + if( idxNum & 0x01 ){ + z1 = "k = ?"; + }else{ + if( idxNum & 0x02 ) { z1 = "k < ?"; } + if( idxNum & 0x04 ) { z1 = "k <= ?"; } + if( idxNum & 0x08 ) { if( z1 ) z2 = "k > ?"; else z1 = "k > ?"; } + if( idxNum & 0x10 ) { if( z1 ) z2 = "k >= ?"; else z1 = "k >= ?"; } + } + + rc = zonefilePrepare(pTab->db, &pCsr->pSelect, &pTab->base.zErrMsg, + "SELECT k, fileid, fofst, fsz, ofst, sz FROM %Q.'%q_shadow_idx'%s%s%s%s", + pTab->zDb, pTab->zName, + z1 ? " WHERE " : "", z1, + z2 ? " AND " : "", z2 + ); + + if( z1 ) sqlite3_bind_value(pCsr->pSelect, 1, argv[0]); + if( z2 ) sqlite3_bind_value(pCsr->pSelect, 2, argv[1]); + + if( rc==SQLITE_OK ){ + rc = zonefileNext(cur); + } + + return rc; +} + +/* +** zonefile virtual table module xEof method. +*/ +static int zonefileEof(sqlite3_vtab_cursor *cur){ + ZonefileCsr *pCsr = (ZonefileCsr*)cur; + return (pCsr->pSelect==0); +} + +static void zonefileFree(void *p){ + sqlite3_free(p); +} + +static int zonefileGetFile( + sqlite3_context *pCtx, /* Leave error message here */ + ZonefileCsr *pCsr, /* Cursor object */ + const char **pzFile /* OUT: Pointer to current file */ +){ + ZonefileTab *pTab = (ZonefileTab*)pCsr->base.pVtab; + int rc = SQLITE_OK; + i64 iFileid; + + if( pTab->pIdToName==0 ){ + rc = zonefilePrepare(pTab->db, &pTab->pIdToName, &pTab->base.zErrMsg, + "SELECT filename FROM %Q.'%q_shadow_file' WHERE fileid=?", + pTab->zDb, pTab->zName + ); + if( rc==SQLITE_ERROR ) zonefileTransferError(pCtx); + } + + if( rc==SQLITE_OK ){ + iFileid = sqlite3_column_int64(pCsr->pSelect,1); + sqlite3_bind_int64(pTab->pIdToName, 1, iFileid); + if( SQLITE_ROW==sqlite3_step(pTab->pIdToName) ){ + *pzFile = (const char*)sqlite3_column_text(pTab->pIdToName, 0); + }else{ + rc = sqlite3_reset(pTab->pIdToName); + if( rc==SQLITE_OK ){ + rc = SQLITE_CORRUPT_VTAB; + }else{ + zonefileTransferError(pCtx); + } + } + } + + return rc; +} + +static void zonefileReleaseFile(ZonefileCsr *pCsr){ + ZonefileTab *pTab = (ZonefileTab*)pCsr->base.pVtab; + sqlite3_reset(pTab->pIdToName); +} + +static int zonefileValueReadDirect(sqlite3_context *pCtx, ZonefileCsr *pCsr){ + i64 iOff = sqlite3_column_int64(pCsr->pSelect, 2); + int sz = sqlite3_column_int(pCsr->pSelect, 3); + + FILE *pFd = 0; /* File handle open on zonefile */ + u8 *aBuf; /* Buffer to read blob into */ + int rc; /* Return code */ + + aBuf = sqlite3_malloc(sz); + if( aBuf==0 ){ + rc = SQLITE_NOMEM; + }else{ + const char *zFile = 0; + /* Open the file to read the blob from */ + rc = zonefileGetFile(pCtx, pCsr, &zFile); + if( rc==SQLITE_OK ){ + char *zErr = 0; + pFd = zonefileFileOpen(zFile, 0, &zErr); + if( pFd==0 ){ + sqlite3_result_error(pCtx, zErr, -1); + sqlite3_free(zErr); + rc = SQLITE_ERROR; + } + zonefileReleaseFile(pCsr); + } + } + + if( rc==SQLITE_OK ){ + rc = zonefileFileRead(pFd, aBuf, sz, iOff); + if( rc==SQLITE_OK ){ + sqlite3_result_blob(pCtx, aBuf, sz, zonefileFree); + aBuf = 0; + } + } + + zonefileFileClose(pFd); + sqlite3_free(aBuf); + return rc; +} + +#ifndef NDEBUG +static void zonefileCacheCheck(ZonefileTab *pTab){ + ZonefileFrame *p; + int n = 0; + for(p=pTab->pCacheFirst; p; p=p->pLruNext){ + assert( p!=pTab->pCacheFirst || p->pLruPrev==0 ); + assert( p!=pTab->pCacheLast || p->pLruNext==0 ); + assert( p==pTab->pCacheFirst || p->pLruPrev->pLruNext==p ); + assert( p==pTab->pCacheLast || p->pLruNext->pLruPrev==p ); + n++; + } + assert( n==pTab->nCacheEntry ); +} +#else +# define zonefileCacheCheck(x) +#endif + +/* +** Search the frame-cache belonging to virtual table pTab for an entry +** corresponding to the frame at byte offset iFrameOff of the file with +** file id iFile. If such an entry is found, return a pointer to it. +** Otherwise, if no such entry exists, return NULL. +*/ +static ZonefileFrame *zonefileCacheFind( + ZonefileTab *pTab, + i64 iFile, + i64 iFrameOff +){ + ZonefileFrame *pFrame; + for(pFrame=pTab->pCacheFirst; pFrame; pFrame=pFrame->pLruNext){ + if( pFrame->iFileid==iFile && pFrame->iFrameOff==iFrameOff ){ + /* Found a match. Move it to the front of the LRU list and return + ** a pointer to it. */ + assert( (pFrame->pLruPrev==0)==(pFrame==pTab->pCacheFirst) ); + assert( (pFrame->pLruNext==0)==(pFrame==pTab->pCacheLast) ); + if( pFrame->pLruPrev ){ + assert( pFrame==pFrame->pLruPrev->pLruNext ); + pFrame->pLruPrev->pLruNext = pFrame->pLruNext; + + if( pFrame->pLruNext ){ + assert( pFrame==pFrame->pLruNext->pLruPrev ); + pFrame->pLruNext->pLruPrev = pFrame->pLruPrev; + }else{ + assert( pFrame==pTab->pCacheLast ); + pTab->pCacheLast = pFrame->pLruPrev; + pTab->pCacheLast->pLruNext = 0; + } + + pFrame->pLruPrev = 0; + pFrame->pLruNext = pTab->pCacheFirst; + pTab->pCacheFirst->pLruPrev = pFrame; + pTab->pCacheFirst = pFrame; + } + + zonefileCacheCheck(pTab); + return pFrame; + } + } + return 0; +} + +/* +** Return the index of the hash bucket that iFileid/iFrameOff belongs to. +*/ +int zonefileCacheHash(ZonefileTab *pTab, i64 iFileid, i64 iFrameOff){ + u32 h; + h = (iFileid & 0xFFFFFFFF) ^ (iFrameOff & 0xFFFFFFFF); + return (h % pTab->nCacheBucket); +} + +/* +** Store the frame object passed as the second argument in the frame +** cache belonging to table pTab. +*/ +static int zonefileCacheStore(ZonefileTab *pTab, ZonefileFrame *pFrame){ + int h; + + /* Allocate the hash table if it has not already been allocated. */ + if( pTab->aCache==0 ){ + int nByte = pTab->nCacheSize * 2 * sizeof(ZonefileFrame*); + pTab->aCache = (ZonefileFrame**)sqlite3_malloc(nByte); + if( pTab->aCache==0 ){ + sqlite3_free(pFrame); + return SQLITE_NOMEM; + } + memset(pTab->aCache, 0, nByte); + pTab->nCacheBucket = pTab->nCacheSize * 2; + } + + /* Add the new entry to the hash table */ + h = zonefileCacheHash(pTab, pFrame->iFileid, pFrame->iFrameOff); + assert( h>=0 && hnCacheBucket ); + pFrame->pHashNext = pTab->aCache[h]; + pTab->aCache[h] = pFrame; + + /* Add the new entry to the LRU list */ + pFrame->pLruPrev = 0; + pFrame->pLruNext = pTab->pCacheFirst; + pTab->pCacheFirst = pFrame; + if( pFrame->pLruNext==0 ){ + pTab->pCacheLast = pFrame; + }else{ + pFrame->pLruNext->pLruPrev = pFrame; + } + pTab->nCacheEntry++; + + if( pTab->nCacheEntry>pTab->nCacheSize ){ + ZonefileFrame **pp; + + /* Remove the oldest entry from the LRU list. */ + ZonefileFrame *pLast = pTab->pCacheLast; + assert( pTab->pCacheLast ); + assert( pTab->nCacheEntry>1 ); + pTab->pCacheLast = pLast->pLruPrev; + pTab->pCacheLast->pLruNext = 0; + pTab->nCacheEntry--; + + /* Remove the same entry from the hash table. */ + h = zonefileCacheHash(pTab, pLast->iFileid, pLast->iFrameOff); + assert( h>=0 && hnCacheBucket ); + for(pp=&pTab->aCache[h]; *pp!=pLast; pp=&((*pp)->pHashNext)); + *pp = pLast->pHashNext; + sqlite3_free(pLast); + } + zonefileCacheCheck(pTab); + + return SQLITE_OK; +} + +/* +** Delete all resources associated with the frame-cache for table pTab. +*/ +static void zonefileCacheDelete(ZonefileTab *pTab){ + ZonefileFrame *p; + ZonefileFrame *pNext; + for(p=pTab->pCacheFirst; p; p=pNext){ + pNext = p->pLruNext; + sqlite3_free(p); + } + sqlite3_free(pTab->aCache); +} + +static int zonefileValueReadCache(sqlite3_context *pCtx, ZonefileCsr *pCsr){ + int rc = SQLITE_OK; + ZonefileTab *pTab = (ZonefileTab*)pCsr->base.pVtab; + ZonefileFrame *pFrame = 0; + i64 iFile = sqlite3_column_int64(pCsr->pSelect, 1); + i64 iFrameOff = sqlite3_column_int64(pCsr->pSelect, 2); + i64 iKeyOff = sqlite3_column_int64(pCsr->pSelect, 4); + int nKeySz = sqlite3_column_int(pCsr->pSelect, 5); + + /* Check if this frame is already in the cache. If not, read it from + ** the file. */ + pFrame = zonefileCacheFind(pTab, iFile, iFrameOff); + if( pFrame==0 ){ + const char *zFile = 0; + char *zErr = 0; + FILE *pFd = 0; + ZonefileHeader hdr = { 0 }; + ZonefileCompress *pCmpMethod = 0; + ZonefileCodec *pCodec = 0; + void *pCmp = 0; + + /* Open the file to read the blob from */ + rc = zonefileGetFile(pCtx, pCsr, &zFile); + if( rc==SQLITE_OK ){ + pFd = zonefileFileOpen(zFile, 0, &zErr); + if( pFd==0 ) rc = SQLITE_ERROR; + } + + /* Read the zonefile header */ + if( rc==SQLITE_OK ){ + rc = zonefileReadHeader(pFd, zFile, &hdr, &zErr); + } + + /* Find the compression method and open the compressor handle. */ + if( rc==SQLITE_OK ){ + rc = zonefileFindCompress(hdr.compressionTypeContent, &pCmpMethod, &zErr); + } + if( pCmpMethod ){ + int nDict = 0; + u8 *aDict = 0; + assert( rc==SQLITE_OK ); + if( hdr.byteOffsetDictionary ){ + nDict = hdr.byteOffsetFrames - hdr.byteOffsetDictionary; + aDict = sqlite3_malloc(nDict); + if( aDict==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = zonefileFileRead(pFd, aDict, nDict, hdr.byteOffsetDictionary); + } + } + if( rc==SQLITE_OK ){ + rc = pCmpMethod->xOpen(&pCmp, aDict, nDict); + } + sqlite3_free(aDict); + } + + /* Find the encryption method and key. */ + if( rc==SQLITE_OK && hdr.encryptionType ){ + const u8 *a = 0; + int n = zonefileKeyFind(pTab->pGlobal, pTab->zDb, pTab->zName, iFile, &a); + if( n==0 ){ + zErr = sqlite3_mprintf("missing encryption key for file \"%s\"", zFile); + rc = SQLITE_ERROR; + }else{ + rc = zonefileCodecCreate(hdr.encryptionType, 0, (u8*)a,n,&pCodec,&zErr); + } + } + + /* Read some data into memory. */ + if( rc==SQLITE_OK ){ + int szFrame = sqlite3_column_int(pCsr->pSelect, 3); + + pFrame = (ZonefileFrame*)sqlite3_malloc(szFrame + sizeof(ZonefileFrame)); + if( pFrame==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(pFrame, 0, sizeof(ZonefileFrame)); + pFrame->aBuf = (u8*)&pFrame[1]; + pFrame->nBuf = szFrame; + pFrame->iFrameOff = iFrameOff; + pFrame->iFileid = iFile; + rc = zonefileFileRead(pFd, pFrame->aBuf, szFrame, iFrameOff); + } + } + + /* Decrypt data if necessary */ + if( rc==SQLITE_OK && pCodec ){ + zonefileCodecDecode(pCodec, pFrame->aBuf, pFrame->nBuf); + pFrame->nBuf -= zonefileCodecNonceSize(pCodec); + } + + /* Uncompress data if required */ + if( rc==SQLITE_OK && pCmpMethod ){ + ZonefileFrame *p = 0; + int nOut = pCmpMethod->xUncompressSize(pCmp, pFrame->aBuf, pFrame->nBuf); + + p = (ZonefileFrame*)sqlite3_malloc(nOut + sizeof(ZonefileFrame)); + if( p==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(p, 0, sizeof(ZonefileFrame)); + p->aBuf = (u8*)&p[1]; + p->nBuf = nOut; + p->iFrameOff = iFrameOff; + p->iFileid = iFile; + rc = pCmpMethod->xUncompress( + pCmp, p->aBuf, p->nBuf, pFrame->aBuf, pFrame->nBuf + ); + sqlite3_free(pFrame); + pFrame = p; + } + } + + if( rc!=SQLITE_OK ){ + sqlite3_free(pFrame); + pFrame = 0; + }else{ + rc = zonefileCacheStore(pTab, pFrame); + if( rc!=SQLITE_OK ) pFrame = 0; + } + zonefileReleaseFile(pCsr); + zonefileFileClose(pFd); + zonefileCodecDestroy(pCodec); + if( pCmpMethod ) pCmpMethod->xClose(pCmp); + + if( zErr ){ + assert( rc!=SQLITE_OK ); + sqlite3_result_error(pCtx, zErr, -1); + sqlite3_free(zErr); + } + } + + if( pFrame ){ + assert( rc==SQLITE_OK ); + sqlite3_result_blob(pCtx, &pFrame->aBuf[iKeyOff], nKeySz, SQLITE_TRANSIENT); + } + + return rc; +} + +/* +** zonefile virtual table module xColumn method. +*/ +static int zonefileColumn( + sqlite3_vtab_cursor *cur, + sqlite3_context *pCtx, + int i +){ + ZonefileCsr *pCsr = (ZonefileCsr*)cur; + int rc = SQLITE_OK; + switch( i ){ + case 0: /* k */ + sqlite3_result_value(pCtx, sqlite3_column_value(pCsr->pSelect, 0)); + break; + case 1: /* v */ + if( sqlite3_column_type(pCsr->pSelect, 5)==SQLITE_NULL ){ + rc = zonefileValueReadDirect(pCtx, pCsr); + }else{ + rc = zonefileValueReadCache(pCtx, pCsr); + } + break; + case 2: /* fileid */ + sqlite3_result_value(pCtx, sqlite3_column_value(pCsr->pSelect, 1)); + break; + default: { /* sz */ + int iCol; + if( sqlite3_column_type(pCsr->pSelect, 5)==SQLITE_NULL ){ + iCol = 3; + }else{ + iCol = 5; + } + sqlite3_result_value(pCtx, sqlite3_column_value(pCsr->pSelect, iCol)); + break; + } + } + return rc; +} + +/* +** zonefile virtual table module xDisconnect method. +*/ +static int zonefileDisconnect(sqlite3_vtab *pVtab){ + ZonefileTab *pTab = (ZonefileTab*)pVtab; + zonefileCacheDelete(pTab); + sqlite3_finalize(pTab->pIdToName); + sqlite3_free(pTab); + return SQLITE_OK; +} + +/* +** zonefile virtual table module xDestroy method. +*/ +static int zonefileDestroy(sqlite3_vtab *pVtab){ + ZonefileTab *pTab = (ZonefileTab*)pVtab; + int rc = SQLITE_OK; + char *zSql = sqlite3_mprintf( + "DROP TABLE IF EXISTS %Q.'%q_shadow_idx';" + "DROP TABLE IF EXISTS %Q.'%q_shadow_file';" + "DROP TABLE IF EXISTS %Q.'%q_shadow_frame';" + "DROP TABLE IF EXISTS %Q.'%q_files';", + pTab->zDb, pTab->zName, pTab->zDb, pTab->zName, + pTab->zDb, pTab->zName, pTab->zDb, pTab->zName + ); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_exec(pTab->db, zSql, 0, 0, 0); + sqlite3_free(zSql); + } + + if( rc==SQLITE_OK ){ + zonefileDisconnect(pVtab); + } + return rc; +} + +/* +** zonefile virtual table module xRowid method. +*/ +static int zonefileRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ + ZonefileCsr *pCsr = (ZonefileCsr*)cur; + *pRowid = sqlite3_column_int64(pCsr->pSelect, 0); + return SQLITE_OK; +} + +/* +** Register the "zonefile" extensions. +*/ +static int zonefileRegister(sqlite3 *db){ + static sqlite3_module filesModule = { + 0, /* iVersion */ + zffCreate, /* xCreate - create a table */ + zffConnect, /* xConnect - connect to an existing table */ + zffBestIndex, /* xBestIndex - Determine search strategy */ + zffDisconnect, /* xDisconnect - Disconnect from a table */ + zffDisconnect, /* xDestroy - Drop a table */ + zffOpen, /* xOpen - open a cursor */ + zffClose, /* xClose - close a cursor */ + zffFilter, /* xFilter - configure scan constraints */ + zffNext, /* xNext - advance a cursor */ + zffEof, /* xEof */ + zffColumn, /* xColumn - read data */ + zffRowid, /* xRowid - read data */ + zffUpdate, /* xUpdate - write data */ + 0, /* xBegin - begin transaction */ + 0, /* xSync - sync transaction */ + 0, /* xCommit - commit transaction */ + 0, /* xRollback - rollback transaction */ + 0, /* xFindFunction - function overloading */ + 0, /* xRename - rename the table */ + 0, /* xSavepoint */ + 0, /* xRelease */ + 0 /* xRollbackTo */ + }; + static sqlite3_module zonefileModule = { + 0, /* iVersion */ + zonefileCreate, /* xCreate - create a table */ + zonefileConnect, /* xConnect - connect to an existing table */ + zonefileBestIndex, /* xBestIndex - Determine search strategy */ + zonefileDisconnect, /* xDisconnect - Disconnect from a table */ + zonefileDestroy, /* xDestroy - Drop a table */ + zonefileOpen, /* xOpen - open a cursor */ + zonefileClose, /* xClose - close a cursor */ + zonefileFilter, /* xFilter - configure scan constraints */ + zonefileNext, /* xNext - advance a cursor */ + zonefileEof, /* xEof */ + zonefileColumn, /* xColumn - read data */ + zonefileRowid, /* xRowid - read data */ + 0, /* xUpdate - write data */ + 0, /* xBegin - begin transaction */ + 0, /* xSync - sync transaction */ + 0, /* xCommit - commit transaction */ + 0, /* xRollback - rollback transaction */ + 0, /* xFindFunction - function overloading */ + 0, /* xRename - rename the table */ + 0, /* xSavepoint */ + 0, /* xRelease */ + 0 /* xRollbackTo */ + }; + + struct Func { + const char *z; + int n; + void (*x)(sqlite3_context*,int,sqlite3_value**); + } aFunc[] = { + { "zonefile_write", 2, zonefileWriteFunc }, + { "zonefile_write", 3, zonefileWriteFunc }, + }; + ZonefileGlobal *pGlobal = 0; + + int rc = SQLITE_OK; + int i; + + for(i=0; rc==SQLITE_OK && iz, p->n, SQLITE_ANY, 0, p->x, 0, 0); + } + + if( rc==SQLITE_OK ){ + pGlobal = (ZonefileGlobal*)sqlite3_malloc(sizeof(ZonefileGlobal)); + if( pGlobal==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(pGlobal, 0, sizeof(ZonefileGlobal)); + } + } + + if( rc==SQLITE_OK ){ + rc = sqlite3_create_module( + db, "zonefile_files", &filesModule, (void*)pGlobal + ); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_create_module_v2(db, "zonefile", &zonefileModule, + (void*)pGlobal, zonefileKeyDestroy + ); + pGlobal = 0; + } + + sqlite3_free(pGlobal); + return rc; +} + +#else /* SQLITE_OMIT_VIRTUALTABLE */ +# define zonefileRegister(x) SQLITE_OK +#endif + +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_zonefile_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ + SQLITE_EXTENSION_INIT2(pApi); + (void)pzErrMsg; /* Unused parameter */ + return zonefileRegister(db); +} ADDED ext/zonefile/zonefile1.test Index: ext/zonefile/zonefile1.test ================================================================== --- /dev/null +++ ext/zonefile/zonefile1.test @@ -0,0 +1,644 @@ +# 2018 Feb 11 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# The focus of this file is testing the zonefile extension. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join $testdir tester.tcl] +set testprefix zonefile1 +load_static_extension db zonefile + +do_execsql_test 1.0 { + CREATE TABLE zz(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + INSERT INTO zz VALUES(1, -1, -1, randomblob(100)); + INSERT INTO zz VALUES(2, -1, -1, randomblob(100)); + INSERT INTO zz VALUES(3, -1, -1, randomblob(100)); +} + +do_execsql_test 1.1 { + SELECT zonefile_write('test.zonefile', 'zz'); +} {{}} + +do_execsql_test 1.2 { + CREATE VIRTUAL TABLE z1 USING zonefile; + SELECT name FROM sqlite_master WHERE name LIKE 'z1%' ORDER BY 1; +} {z1 z1_files z1_shadow_file z1_shadow_idx} + +do_execsql_test 1.3 { + INSERT INTO z1_files(filename) VALUES('test.zonefile'); + SELECT filename, + json_extract(header, '$.magicNumber'), + json_extract(header, '$.numFrames'), + json_extract(header, '$.numKeys') + FROM z1_files; +} {test.zonefile 1179332920 1 3} + +do_execsql_test 1.4 { SELECT count(*) FROM z1_shadow_idx } 3 + +do_execsql_test 1.5.1 { SELECT k FROM z1 } {1 2 3} +do_execsql_test 1.5.2 { SELECT fileid FROM z1 } {1 1 1} +do_execsql_test 1.5.4 { SELECT sz FROM z1 } {100 100 100} + +do_execsql_test 1.5.5 { + SELECT zz.v==z1.v FROM zz, z1 WHERE zz.k=z1.k +} {1 1 1} + +do_execsql_test 1.5 { + DELETE FROM z1_files; + SELECT * FROM z1_files; +} {} + +do_execsql_test 1.6 { SELECT count(*) FROM z1_shadow_idx } 0 + +do_execsql_test 1.7 { DROP TABLE z1 } + +do_execsql_test 1.8 { + SELECT * FROM sqlite_master WHERE name LIKE 'z1%'; +} + +#------------------------------------------------------------------------- +# Figure out which compression algorithms, if any, are supported by +# this build. Populate the global list $COMPRESSION_METHODS with the +# result. +reset_db +load_static_extension db zonefile +do_execsql_test 2.0 { + CREATE TABLE bb( + k INTEGER PRIMARY KEY, + frame INTEGER DEFAULT -1, + idx INTEGER DEFAULT -1, + v BLOB + ); + INSERT INTO bb(k, v) VALUES(1, randomblob(100)); +} +set COMPRESSION_METHODS [list] +foreach cmp { + none zlib zstd zstd_global_dict lz4 lz4hc brotli nosuchcmp +} { + set res [catchsql { + WITH p(n,v) AS ( + VALUES('compressionTypeContent', $cmp) + ) + SELECT zonefile_write('test.zonefile', 'bb', json_group_object(n,v)) FROM p; + }] + + if {[lindex $res 0]==0} { + lappend COMPRESSION_METHODS $cmp + } +} + +# Check that it is not possible to use zstd_global_dict to compress +# the zonefile index. +# +if {[lsearch $COMPRESSION_METHODS zstd_global_dict]>=0} { + do_catchsql_test 2.1 { + WITH p(n,v) AS ( + VALUES('compressionTypeIndexData', 'zstd_global_dict') + ) + SELECT zonefile_write('test.zonefile', 'bb', json_group_object(n,v)) FROM p; + } {1 {compressor "zstd_global_dict" may not be used to compress the zonefile index}} +} + +set extra_header 0 +set cachesize 0 +foreach cmp $COMPRESSION_METHODS { foreach cmpidx $COMPRESSION_METHODS { + if {$cmpidx == "zstd_global_dict"} continue + reset_db + load_static_extension db zonefile + + set tn "$cmp/$cmpidx" + set extra_header [expr {$extra_header ? 0 : 100}] + set cachesize [expr {$cachesize ? 0 : 10}] + + do_execsql_test 2.$tn.0.1 { + CREATE TABLE zz( + k INTEGER PRIMARY KEY, + frame INTEGER DEFAULT -1, + idx INTEGER DEFAULT -1, + v BLOB + ); + CREATE TABLE rt(k INTEGER PRIMARY KEY, v BLOB); + } + + do_execsql_test 2.$tn.0.2 " + CREATE VIRTUAL TABLE zone USING zonefile(cachesize = $cachesize) + " {} + + set nMinByte 0 + set nMaxByte 444 + foreach {zonefile lKey} { + test1.zonefile {195 1238 298 405 297} + test2.zonefile {124 1624 82 1929} + test3.zonefile {932 683 1751 410 41} + test4.zonefile {427 1491} + test5.zonefile {1004 473 801 394 1672 816 1577} + test6.zonefile {1374 1454 1005} + test7.zonefile {450 241 319 133} + test8.zonefile {1414 900 1406 1917 127 673} + test9.zonefile {1192 226 988 1292 718 1345 1675} + test10.zonefile {314} + test11.zonefile {1177 1597 60 532 291 1164 812} + test12.zonefile {1168 1290 1585 939 1916} + test13.zonefile {644 1784 1476 1283 433 506} + test14.zonefile {1141 1547 1506 364} + test15.zonefile {1756 1885 844 1880 1896 354} + test16.zonefile {1383 1928 1371} + test17.zonefile {93} + test18.zonefile {1067} + test19.zonefile {642} + test20.zonefile {1380 1857} + test21.zonefile {288 293 1968 1207 1739 231 300} + test22.zonefile {651 1007 607 830 299 1431} + test23.zonefile {81 1651 543 1949 256 119 1088} + test24.zonefile {1278 2024 682 1115 194 636 1804} + test25.zonefile {514 1155 171 2015 791} + test26.zonefile {1615 1228 147 1464} + test27.zonefile {55 1130 781 678 78} + test28.zonefile {1981 1401 1178} + test29.zonefile {1754 864 183 1953 1901} + test30.zonefile {1461 817} + test31.zonefile {1720 1722 686 1833} + } { + forcedelete $zonefile + execsql { DELETE FROM zz; } + foreach k $lKey { + execsql { INSERT INTO zz(k, v) VALUES($k, randomblob($k)) } + } + execsql { INSERT INTO rt SELECT k, v FROM zz } + execsql { + WITH p(n,v) AS ( + VALUES('maxAutoFrameSize', 2000) UNION ALL + VALUES('compressionTypeIndexData', $cmpidx) UNION ALL + VALUES('compressionTypeContent', $cmp) UNION ALL + VALUES('debugExtendedHeaderSize', $extra_header) + ) + SELECT zonefile_write($zonefile, 'zz', json_group_object(n, v)) FROM p; + INSERT INTO zone_files(filename) VALUES($zonefile); + } + } + + do_execsql_test 2.$tn.1 { + SELECT k FROM zone JOIN rt USING (k) WHERE zone.v!=rt.v + } + do_execsql_test 2.$tn.2 { + SELECT count(*) FROM zone JOIN rt USING (k); + } {135} + do_execsql_test 2.$tn.3 { + SELECT filename, + json_extract(header, '$.numKeys'), + json_extract(header, '$.numFrames') + FROM zone_files + WHERE filename IN ('test19.zonefile', 'test20.zonefile', 'test21.zonefile') + ORDER BY 1 + } { + test19.zonefile 1 1 + test20.zonefile 2 2 + test21.zonefile 7 4 + } +}} + +#-------------------------------------------------------------------------- +# +reset_db +load_static_extension db zonefile +do_execsql_test 3.0 { + CREATE TABLE dd(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + INSERT INTO dd VALUES(1000, 1, -1, randomblob(44)); + INSERT INTO dd VALUES(1001, 1, -1, randomblob(55)); + INSERT INTO dd VALUES(1002, 2, -1, randomblob(66)); + WITH p(n,v) AS ( + VALUES('maxAutoFrameSize', 2000) UNION ALL + VALUES('encryptionType', 'xor') UNION ALL + VALUES('debugEncryptionKeyText', 1) UNION ALL + VALUES('encryptionKey', '0123456789') + ) + SELECT zonefile_write('test.zonefile', 'dd', json_group_object(n, v)) FROM p; +} {{}} + +do_execsql_test 3.1 { + CREATE VIRTUAL TABLE cc USING zonefile; + INSERT INTO cc_files(filename,ekey) VALUES('test.zonefile','0123456789'); + SELECT quote(dd.v)==quote(cc.v) FROM cc JOIN dd USING (k) +} {1 1 1} + +do_execsql_test 3.2.1 { + DELETE FROM cc_files; + INSERT INTO cc_files(filename,ekey) VALUES('test.zonefile','abcdefghij'); + SELECT quote(dd.v)==quote(cc.v) FROM cc JOIN dd USING (k) +} {0 0 0} + +do_execsql_test 3.2.2 { + SELECT rowid,sz FROM cc; +} {1000 44 1001 55 1002 66} + +do_execsql_test 3.3 { + UPDATE cc_files SET ekey = '0123456789'; + SELECT quote(dd.v)==quote(cc.v) FROM cc JOIN dd USING (k) +} {1 1 1} + +close [open test.zonefile w+] +do_catchsql_test 3.4 { + SELECT header FROM cc_files +} {1 {failed to read zonefile header from file "test.zonefile"}} + +forcedelete test.zonefile +do_catchsql_test 3.5 { + SELECT header FROM cc_files +} {1 {failed to open file "test.zonefile" for reading}} + +do_execsql_test 3.6 { + SELECT ekey FROM cc_files +} {{}} + +forcedelete test.zonefile +do_catchsql_test 3.7 { + SELECT * FROM cc; +} {1 {failed to open file "test.zonefile" for reading}} + +#------------------------------------------------------------------------- +# Check that a file that uses an unknown compression method is handled +# correctly (an error is returned). +# +reset_db +load_static_extension db zonefile +do_execsql_test 4.0 { + CREATE TABLE dd(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + INSERT INTO dd VALUES(1000, 1, -1, randomblob(44)); + INSERT INTO dd VALUES(1001, 1, -1, randomblob(55)); + INSERT INTO dd VALUES(1002, 2, -1, randomblob(66)); + SELECT zonefile_write('test.zonefile', 'dd'); + CREATE VIRTUAL TABLE x1 USING zonefile; +} {{}} + +do_test 4.1 { + hexio_write test.zonefile 5 77 +} {1} +do_execsql_test 4.2 { + INSERT INTO x1_files(filename) VALUES('test.zonefile'); +} {} +do_catchsql_test 4.3 { + SELECT * FROM x1 +} {1 {unsupported compression method: 119}} +do_test 4.4 { + hexio_write test.zonefile 4 77 +} {1} +do_catchsql_test 4.5 { + DELETE FROM x1_files; + INSERT INTO x1_files(filename) VALUES('test.zonefile'); +} {1 {unsupported compression method: 119}} + +do_test 4.6 { + hexio_write test.zonefile 0 00 +} {1} +do_catchsql_test 4.7 { + INSERT INTO x1_files(filename) VALUES('test.zonefile'); +} {1 {failed to read zonefile header from file "test.zonefile"}} + +#------------------------------------------------------------------------- +# Test using various types in the "frame" field of an input table. +# +reset_db +load_static_extension db zonefile +if {[lsearch $COMPRESSION_METHODS zlib]>=0} { + do_execsql_test 5.0 { + CREATE TABLE "a b"(k INTEGER PRIMARY KEY,frame INTEGER,idx INTEGER,v BLOB); + INSERT INTO "a b" VALUES(1, 0.5, -1, randomblob(44)); + INSERT INTO "a b" VALUES(2, 0.5, -1, randomblob(55)); + INSERT INTO "a b" VALUES(3, 1.5, -1, randomblob(55)); + INSERT INTO "a b" VALUES(4, 1.5, -1, randomblob(55)); + INSERT INTO "a b" VALUES(5, 2, -1, randomblob(55)); + INSERT INTO "a b" VALUES(6, 2, -1, randomblob(55)); + INSERT INTO "a b" VALUES(7, 200, -1, randomblob(55)); + INSERT INTO "a b" VALUES(8, 200, -1, randomblob(55)); + INSERT INTO "a b" VALUES(9, 300, -1, randomblob(55)); + INSERT INTO "a b" VALUES(10, 300, -1, randomblob(55)); + INSERT INTO "a b" VALUES(11, NULL, -1, randomblob(55)); + INSERT INTO "a b" VALUES(12, NULL, -1, randomblob(55)); + INSERT INTO "a b" VALUES(13, 'f1', -1, randomblob(55)); + INSERT INTO "a b" VALUES(14, 'f1', -1, randomblob(55)); + INSERT INTO "a b" VALUES(15, 'frame2', -1, randomblob(55)); + INSERT INTO "a b" VALUES(16, 'frame2', -1, randomblob(55)); + INSERT INTO "a b" VALUES(17, x'1234', -1, randomblob(55)); + INSERT INTO "a b" VALUES(18, x'1234', -1, randomblob(55)); + INSERT INTO "a b" VALUES(19, x'abcd', -1, randomblob(55)); + INSERT INTO "a b" VALUES(20, x'abcd', -1, randomblob(55)); + + SELECT zonefile_write('test.zonefile', 'a b', + '{"compressionTypeContent":"zlib"}' + ); + } {{}} + + do_execsql_test 5.1 { + CREATE VIRTUAL TABLE abc USING zonefile; + INSERT INTO abc_files(filename) VALUES('test.zonefile'); + SELECT group_concat(k) FROM abc_shadow_idx GROUP BY fofst + } { + 11,12 1,2 3,4 5,6 7,8 + 9,10 13,14 15,16 17,18 19,20 + } +} + +do_execsql_test 6.0 { + CREATE TABLE "ab"(k INTEGER PRIMARY KEY,frame INTEGER,idx INTEGER,v BLOB); + INSERT INTO "ab" VALUES(1, 0.5, -1, randomblob(44)); + INSERT INTO "ab" VALUES(2, 0.5, -1, randomblob(55)); + INSERT INTO "ab" VALUES(3, 1.5, -1, randomblob(55)); + INSERT INTO "ab" VALUES(4, 1.5, -1, randomblob(55)); +} +do_catchsql_test 6.1.1 { + SELECT zonefile_write('test.zonefile', 'ab', + '{"debugExtendedHeaderSize":-1}' + ); +} {1 {debugExtendedHeaderSize value out of range: -1}} +do_catchsql_test 6.1.2 { + SELECT zonefile_write('test.zonefile', 'ab', + '{"debugExtendedHeaderSize":256}' + ); +} {1 {debugExtendedHeaderSize value out of range: 256}} + +do_catchsql_test 6.2 { + SELECT zonefile_write('test.zonefile', 'ab', + '{"unknown":256}' + ); +} {1 {unknown parameter name: "unknown"}} + +forcedelete test.dir +file mkdir test.dir +do_catchsql_test 6.3 { + SELECT zonefile_write('test.dir', 'ab'); +} {1 {failed to open file "test.dir" for writing}} + +do_catchsql_test 6.4 { + CREATE VIRTUAL TABLE zzz USING zonefile; + INSERT INTO zzz_files(filename) VALUES('nosuchfile.zonefile'); +} {1 {failed to open file "nosuchfile.zonefile" for reading}} + +if {$tcl_platform(platform)=="windows"} { + do_catchsql_test 6.5 { + INSERT INTO zzz_files(filename) VALUES('test.dir'); + } {1 {failed to open file "test.dir" for reading}} +} else { + do_catchsql_test 6.5 { + INSERT INTO zzz_files(filename) VALUES('test.dir'); + } {1 {failed to read zonefile header from file "test.dir"}} +} + +#------------------------------------------------------------------------- +# Check that errors generated when building a dictionary are handled. +# The zstd library routines for building a dictionary throw an error +# if they are provided with too little data. +# +# Also test that zstd_global_dict cannot be used to compress the zonefile +# index (as there is nowhere in the file format to store the dictionary +# for this compression). +# +reset_db +load_static_extension db zonefile +if {[lsearch $COMPRESSION_METHODS zstd_global_dict]>=0} { + do_execsql_test 7.0 { + CREATE TABLE "ab"(k INTEGER PRIMARY KEY,frame INTEGER,idx INTEGER,v BLOB); + INSERT INTO "ab" VALUES(1, -1, -1, 'abc'); + } + + do_catchsql_test 7.1 { + SELECT zonefile_write('test.zonefile', 'ab', + '{"compressionTypeContent":"zstd_global_dict"}' + ); + } {1 {error generating dictionary}} + + do_catchsql_test 7.2 { + SELECT zonefile_write('test.zonefile', 'ab', + '{"compressionTypeIndexData":"zstd_global_dict"}' + ); + } {1 {compressor "zstd_global_dict" may not be used to compress the zonefile index}} +} + +#------------------------------------------------------------------------- +# +reset_db +load_static_extension db zonefile +do_catchsql_test 8.1 { + CREATE VIRTUAL TABLE one USING zonefile_files; +} {1 {do not create zonefile_files tables directly!}} +do_catchsql_test 8.2 { + CREATE VIRTUAL TABLE onetwothree USING zonefile_files; +} {1 {do not create zonefile_files tables directly!}} + +#------------------------------------------------------------------------- +# A zone file containing zero keys. +# +reset_db +load_static_extension db zonefile +do_execsql_test 9.0 { + CREATE TABLE s(k INTEGER PRIMARY KEY,frame INTEGER,idx INTEGER,v BLOB); + SELECT zonefile_write('test.zonefile', 's'); +} {{}} +do_execsql_test 9.1 { + CREATE VIRTUAL TABLE ss USING zonefile; + INSERT INTO ss_files(filename) VALUES('test.zonefile'); + SELECT * FROM ss; +} {} + +#------------------------------------------------------------------------- +# Test UPDATE and DELETE statements on the %_files virtual table. +# +reset_db +load_static_extension db zonefile +do_execsql_test 10.0 { + CREATE TABLE data(k INTEGER PRIMARY KEY, v BLOB); + INSERT INTO data(k, v) VALUES(1, randomblob(100)); + INSERT INTO data(k, v) VALUES(2, randomblob(100)); + INSERT INTO data(k, v) VALUES(3, randomblob(100)); + INSERT INTO data(k, v) VALUES(4, randomblob(100)); + INSERT INTO data(k, v) VALUES(5, randomblob(100)); + INSERT INTO data(k, v) VALUES(6, randomblob(100)); + INSERT INTO data(k, v) VALUES(7, randomblob(100)); + INSERT INTO data(k, v) VALUES(8, randomblob(100)); + INSERT INTO data(k, v) VALUES(9, randomblob(100)); + + CREATE VIEW v1 AS + SELECT k, -1 AS frame, -1 AS idx, v + FROM data WHERE k IN (1,2,3); + SELECT zonefile_write('test1.zonefile', 'v1'); + + CREATE VIEW v2 AS + SELECT k, -1 AS frame, -1 AS idx, v + FROM data WHERE k IN (4,5,6); + SELECT zonefile_write('test2.zonefile', 'v2'); + + CREATE VIEW v3 AS + SELECT k, -1 AS frame, -1 AS idx, v + FROM data WHERE k IN (7,8,9); + SELECT zonefile_write('test3.zonefile', 'v3'); +} {{} {} {}} + +do_execsql_test 10.1 { + CREATE VIRTUAL TABLE "a b" USING zonefile; + INSERT INTO "a b_files"(filename) VALUES('test1.zonefile'); + SELECT k FROM "a b" +} {1 2 3} + +do_execsql_test 10.2 { + UPDATE "a b_files" SET filename = 'test2.zonefile'; + SELECT k FROM "a b" +} {4 5 6} + +do_execsql_test 10.3 { + INSERT INTO "a b_files"(filename) VALUES('test1.zonefile'); + INSERT INTO "a b_files"(filename) VALUES('test3.zonefile'); + SELECT k FROM "a b" +} {1 2 3 4 5 6 7 8 9} + +do_execsql_test 10.4 { + DELETE FROM "a b_files" WHERE filename = 'test2.zonefile'; + SELECT k FROM "a b" +} {1 2 3 7 8 9} + +do_execsql_test 10.5 { + DELETE FROM "a b_files" WHERE filename = 'test1.zonefile'; + SELECT k FROM "a b" +} {7 8 9} + +#------------------------------------------------------------------------- +# +reset_db +load_static_extension db zonefile +do_execsql_test 11.0 { + CREATE TABLE data(k INTEGER PRIMARY KEY, v BLOB); + INSERT INTO data(k, v) VALUES(-1, randomblob(100)); + INSERT INTO data(k, v) VALUES(1000, randomblob(100)); + INSERT INTO data(k, v) VALUES(-1000, randomblob(100)); + INSERT INTO data(k, v) VALUES(9223372036854775807, randomblob(100)); + INSERT INTO data(k, v) VALUES(-9223372036854775807, randomblob(100)); + INSERT INTO data(k, v) VALUES(-9223372036854775808, randomblob(100)); + + CREATE VIEW v1 AS SELECT k, -1 AS frame, -1 AS idx, v FROM data; + SELECT zonefile_write('test1.zonefile', 'v1'); +} {{}} + +do_execsql_test 11.1 { + CREATE VIRTUAL TABLE one USING zonefile; + INSERT INTO "one_files"(filename) VALUES('test1.zonefile'); + SELECT k FROM one +} {-9223372036854775808 -9223372036854775807 -1000 -1 1000 9223372036854775807} + +do_execsql_test 11.2 { + SELECT count(*) FROM data JOIN one USING (k) WHERE one.v==data.v +} 6 + +#------------------------------------------------------------------------- +# +do_catchsql_test 12.1 { + CREATE VIRTUAL TABLE nm USING zonefile(abcd) +} {1 {parse error in option: abcd}} +do_catchsql_test 12.2 { + CREATE VIRTUAL TABLE nm USING zonefile(a=b) +} {1 {parse error in option: a=b}} +do_catchsql_test 12.3 { + CREATE VIRTUAL TABLE nm USING zonefile(cachesiza=b) +} {1 {parse error in option: cachesiza=b}} + +#------------------------------------------------------------------------- +# +reset_db +load_static_extension db zonefile +do_execsql_test 13.0 { + CREATE TABLE data(k INTEGER PRIMARY KEY, v BLOB); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<250 + ) + INSERT INTO data SELECT i, randomblob(100) FROM s; + CREATE VIEW v1 AS SELECT k, -1 AS frame, -1 AS idx, v FROM data; + SELECT zonefile_write('test1.zonefile', 'v1'); + + CREATE VIRTUAL TABLE nm USING zonefile; + INSERT INTO nm_files(filename) VALUES('test1.zonefile'); +} {{}} + +foreach {tn cond} { + 1 "k > 30" + 2 "k >= 100" + 3 "k <= 100" + 4 "k < 55" + 5 "k LIKE '1%'" + 6 "k BETWEEN 10 AND 20" + 7 "k > 100 AND k < 200" +} { + do_execsql_test 13.1.$tn.1 [subst { + SELECT count(*) FROM nm WHERE $cond + }] [db one "SELECT count(*) FROM data WHERE $cond"] + + do_execsql_test 13.1.$tn.2 [subst { + SELECT count(*) FROM nm WHERE $cond AND + v!=(SELECT v FROM data WHERE k=nm.k); + }] 0 +} + +close [open test1.zonefile w+] +do_catchsql_test 13.2.1 { + SELECT * FROM nm WHERE k=24; +} {1 {SQL logic error}} +forcedelete test1.zonefile +do_catchsql_test 13.2.2 { + SELECT * FROM nm WHERE k=24; +} {1 {failed to open file "test1.zonefile" for reading}} + +do_catchsql_test 13.3.1 { + DELETE FROM nm_shadow_file; + SELECT * FROM nm WHERE k=24; +} {1 {database disk image is malformed}} +do_catchsql_test 13.3.2 { + DROP TABLE nm_shadow_file; + SELECT * FROM nm WHERE k=24; +} {1 {no such table: main.nm_shadow_file}} +db close +sqlite3 db test.db +load_static_extension db zonefile +do_catchsql_test 13.3.3 { + SELECT * FROM nm WHERE k=24; +} {1 {no such table: main.nm_shadow_file}} + +#------------------------------------------------------------------------- +# +reset_db +load_static_extension db zonefile +do_execsql_test 14.0 { + CREATE TABLE data(k INTEGER PRIMARY KEY, frame, idx, v BLOB); + INSERT INTO data VALUES(1, 1, -1, randomblob(200)); + INSERT INTO data VALUES(2, 2, -1, randomblob(200)); + INSERT INTO data VALUES(3, 3, -1, randomblob(200)); + SELECT zonefile_write('test.zonefile', 'data', + '{"encryptionType":"xor","encryptionKey":"pass","debugEncryptionKeyText":1}' + ); + + CREATE VIRTUAL TABLE nm USING zonefile(cachesize=2); + INSERT INTO nm_files(filename,ekey) VALUES('test.zonefile','pass'); +} {{}} + +set i 0 +foreach id {1 2 3 2 3 1} { + do_execsql_test 14.1.$i { + SELECT data.v=nm.v FROM data,nm WHERE data.k=$id AND nm.k=$id + } 1 + incr i +} + +if {[file exists /dev/null]} { + do_catchsql_test 14.2 { + INSERT INTO nm_files(filename) VALUES('/dev/null'); + } {1 {failed to read zonefile header from file "/dev/null"}} +} + +finish_test + ADDED ext/zonefile/zonefileenc.test Index: ext/zonefile/zonefileenc.test ================================================================== --- /dev/null +++ ext/zonefile/zonefileenc.test @@ -0,0 +1,285 @@ +# 2018 Feb 11 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# The focus of this file is testing the zonefile extension. +# + +package require Tcl 8.6 + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join $testdir tester.tcl] +set testprefix zonefileenc + + +foreach {tn code} { + 1 { + set K { + braking bramble brambles brambly + bran branch branched branches + branching branchings brand branded + } + set textkey 1 + } + 2 { + set K { + 5e008542742ce0442e37cbf2512e9492 c91c26e0573ca3464e037568c51126da + e90e17489c1aef80ac620c9059271a5a 163338707cbe4c72b18d1058a42c5c78 + 5c6b1e7c7c9e8e4a8d8fdc30dfc11bea ff1012687828ecaac6c9ca86ea0f895e + a203f25eb11d4c6afa841dfcf7cd0be0 b6c71e38ca914c460926ef90db39dba0 + b38255d031d026c258a0a41a9a75d46a adccca5e5ffa3a7625144a345713aef0 + cd423b38b73e42ce5894405e6d0e08c0 b460ad2e370a0386726d6ea46e7b0bac + 503b81de72cb3ef87d9346a850040000 369c290a464a6b88bfd9d1c4755afd42 + a8a9343efca528f2bf23a972be49dd66 e366b5226bfe3fd0010fa814aae3b996 + 4cad7e80124c2cd447131bae377e60f6 4a0fd2f054e1b08cad0de2dc6aa93246 + 8a23c85e3337da2c97d498f806870fa8 8d14e1f055fd9bec7d07cf0e8baae042 + 7f6954b0dc373028ab3b030aaf44dd58 d220164c3898435a946de6bcbb478cc4 + 566af7ea88ba4ff87fd868e858cf98ea a5405832235e8f601516f9c49767bdac + 1bd5b4dc6b54e5ca92ba67d20bf65740 59da30e203bf73840e38e108b83ddb82 + e516924c2cdf3114f10f2f0e1bdabbc6 b55dd27222a39764222838007e749984 + 190ae9f81b86a5a024e3b97ee2a7121c 469660843a9a9e507d0fb43e92029296 + e6e600bc063aad12f6387beef650c48a 3097be5c3a52a2f00747587add01b550 + } + set textkey 0 + } +} { + reset_db + load_static_extension db zonefile + set nFile 100 + eval $code + + do_execsql_test 1.$tn.0 { + CREATE TABLE zz(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + CREATE TABLE rr(k INTEGER PRIMARY KEY, v); + } + do_test 1.$tn.1 { + for {set i 0} {$i < $nFile} {incr i} { + set k [lindex $K [expr $i % [llength $K]]] + execsql { + DELETE FROM zz; + INSERT INTO zz VALUES($i*10+1, 1, -1, randomblob(100)); + INSERT INTO zz VALUES($i*10+2, 2, -1, randomblob(100)); + INSERT INTO zz VALUES($i*10+3, 1, -1, randomblob(100)); + INSERT INTO rr SELECT k,v FROM zz; + + WITH p(n,v) AS ( + VALUES('encryptionType', 'xor') UNION ALL + VALUES('debugEncryptionKeyText', $textkey) UNION ALL + VALUES('encryptionKey', $k) + ) + SELECT zonefile_write('test' || $i || '.zonefile', 'zz', + json_group_object(n, v) + ) FROM p; + } + } + } {} + + proc k {i} { + set val [lindex $::K [expr $i % [llength $::K]]] + if {$::textkey==0} { + return [binary decode hex $val] + } + return $val + } + db func k k + + do_execsql_test 1.$tn.2 { + CREATE VIRTUAL TABLE gg USING zonefile; + } + for {set i 0} {$i < $nFile} {incr i} { + do_execsql_test 1.$tn.2.$i { + INSERT INTO gg_files(filename, ekey) + VALUES('test' || $i || '.zonefile', k($i)); + SELECT count(*) FROM rr JOIN gg USING(k) WHERE rr.v!=gg.v; + } 0 + } + + db close + sqlite3 db test.db + load_static_extension db zonefile + db func k k + + do_catchsql_test 1.$tn.3 { + SELECT count(*) FROM rr JOIN gg USING(k) WHERE rr.v!=gg.v; + } {1 {missing encryption key for file "test0.zonefile"}} + do_execsql_test 1.$tn.4 { + UPDATE gg_files SET ekey = k(0) WHERE filename='test0.zonefile'; + } + do_execsql_test 1.$tn.4.2 { + SELECT count(*) FROM rr JOIN gg USING(k) + WHERE rr.v==gg.v AND k IN (1,2,3); + } {3} + do_catchsql_test 1.5 { + SELECT count(*) FROM rr JOIN gg USING(k) WHERE rr.v!=gg.v; + } {1 {missing encryption key for file "test1.zonefile"}} + + do_execsql_test 1.$tn.6 { + UPDATE gg_files SET ekey = k(rowid-1); + } + do_execsql_test 1.$tn.7 { + SELECT count(*) FROM rr JOIN gg USING(k) WHERE rr.v!=gg.v; + } {0} + do_execsql_test 1.$tn.8 { + SELECT count(*) FROM rr JOIN gg USING(k) WHERE rr.v==gg.v; + } {300} + + forcedelete test.db2 + do_execsql_test 1.$tn.9.1 { + ATTACH 'test.db2' AS maing; + CREATE VIRTUAL TABLE maing.g USING zonefile; + INSERT INTO g_files(filename) SELECT filename FROM gg_files; + } + do_catchsql_test 1.$tn.9.2 { + SELECT count(*) FROM rr JOIN g USING(k) WHERE rr.v!=g.v; + } {1 {missing encryption key for file "test0.zonefile"}} + do_execsql_test 1.$tn.9.3 { + UPDATE g_files SET ekey = k(rowid-1); + SELECT count(*) FROM rr JOIN g USING(k) WHERE rr.v==g.v; + } {300} + + do_execsql_test 1.$tn.10 { + SELECT count(*) FROM rr JOIN gg USING(k) WHERE rr.v==gg.v; + } {300} +} +#------------------------------------------------------------------------- + +reset_db +load_static_extension db zonefile + +do_execsql_test 2.0 { + CREATE TABLE zz(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); +} +foreach {tn alg id} { + 1 aes_128_ctr 1 + 2 aes_128_cbc 2 + 3 AES_256_CTR 3 + 4 Aes_256_CBC 4 +} { + do_catchsql_test 2.1.$tn { + WITH p(n,v) AS ( + VALUES('encryptionType', $alg) UNION ALL + VALUES('debugEncryptionKeyText', 1) UNION ALL + VALUES('encryptionKey', 'secret') + ) + SELECT zonefile_write('test' || $i || '.zonefile', 'zz', + json_group_object(n, v) + ) FROM p; + } "1 {unsupported encryption method: $id}" +} + +foreach {tn alg} { + 1 nosuchmethod! +} { + do_catchsql_test 2.2.$tn { + WITH p(n,v) AS ( + VALUES('encryptionType', $alg) UNION ALL + VALUES('debugEncryptionKeyText', 1) UNION ALL + VALUES('encryptionKey', 'secret') + ) + SELECT zonefile_write('test' || $i || '.zonefile', 'zz', + json_group_object(n, v) + ) FROM p; + } "1 {unknown encryption method: $alg}" +} + +#------------------------------------------------------------------------- +# Test some hash collisions in the encryption key table. +# + +# This is the same hash function used internally to store keys. +# +proc hash {zDb zTab iFile} { + binary scan $zDb c* A + binary scan $zTab c* B + set h 0 + foreach i $A { set h [expr ($h + ($h << 3) + $i) & 0xFFFFFFFF] } + foreach i $B { set h [expr ($h + ($h << 3) + $i) & 0xFFFFFFFF] } + return [expr $h ^ $iFile] +} + +do_test 3.0 { + set h1 [expr [hash main zone 1] % 512] + for {set i 0} {1} {incr i} { + set h2 [expr [hash "aux$i" zone 1] % 512] + if {$h1==$h2} break + } + set i +} 52 + +reset_db +load_static_extension db zonefile +forcedelete test.db2 + +do_execsql_test 3.1 { + CREATE TABLE zz(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + INSERT INTO zz VALUES(222, -1, -1, randomblob(60)); + WITH p(n,v) AS ( + VALUES('encryptionType', 'xor') UNION ALL + VALUES('debugEncryptionKeyText', 1) UNION ALL + VALUES('encryptionKey', 'pass') + ) + SELECT zonefile_write('test1.zonefile', 'zz', + json_group_object(n, v) + ) FROM p; + + DELETE FROM zz; + INSERT INTO zz VALUES(333, -1, -1, randomblob(80)); + WITH p(n,v) AS ( + VALUES('encryptionType', 'xor') UNION ALL + VALUES('debugEncryptionKeyText', 1) UNION ALL + VALUES('encryptionKey', 'pass') + ) + SELECT zonefile_write('test2.zonefile', 'zz', + json_group_object(n, v) + ) FROM p; +} {{} {}} + +do_execsql_test 3.2 { + ATTACH 'test.db2' AS aux52; + CREATE VIRTUAL TABLE main.zone USING zonefile; + CREATE VIRTUAL TABLE aux52.zone USING zonefile; + INSERT INTO main.zone_files(filename, ekey) VALUES('test1.zonefile', 'pass'); + INSERT INTO aux52.zone_files(filename, ekey) VALUES('test2.zonefile', 'pass'); +} + +do_execsql_test 3.3 { + SELECT v IS NULL FROM main.zone; + SELECT v IS NULL FROM aux52.zone; +} {0 0} + +do_test 3.4 { + set h1 [expr [hash main zone 1] % 512] + for {set i 0} {1} {incr i} { + set h2 [expr [hash "aux$i" zone 2] % 512] + if {$h1==$h2} break + } + set i +} 682 + +forcedelete test.db3 +do_execsql_test 3.5 { + ATTACH 'test.db3' AS aux682; + CREATE VIRTUAL TABLE aux682.zone USING zonefile; + INSERT INTO aux682.zone_files(filename, ekey) VALUES('test1.zonefile','pass'); + INSERT INTO aux682.zone_files(filename, ekey) VALUES('test2.zonefile','pass'); + INSERT INTO main.zone_files(filename, ekey) VALUES('test2.zonefile','pass'); +} + +do_execsql_test 3.6 { + SELECT v IS NULL FROM main.zone; + SELECT v IS NULL FROM aux682.zone; + SELECT v IS NULL FROM main.zone; +} {0 0 0 0 0 0} + + +finish_test + ADDED ext/zonefile/zonefilefault.test Index: ext/zonefile/zonefilefault.test ================================================================== --- /dev/null +++ ext/zonefile/zonefilefault.test @@ -0,0 +1,251 @@ +# 2018 Feb 11 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# The focus of this file is testing the zonefile extension. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join $testdir tester.tcl] +source [file join $testdir malloc_common.tcl] +set testprefix zonefilefault +load_static_extension db zonefile + +do_execsql_test 1.0 { + CREATE TABLE tt(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + INSERT INTO tt VALUES(1, -1, -1, randomblob(100)); + INSERT INTO tt VALUES(2, -1, -1, randomblob(200)); + INSERT INTO tt VALUES(3, 1, -1, randomblob(300)); + INSERT INTO tt VALUES(4, 2, -1, randomblob(400)); +} + +do_faultsim_test 1.1 -faults oom* -prep { + sqlite3 db test.db + load_static_extension db zonefile +} -body { + execsql { + SELECT zonefile_write('test.zonefile', 'tt', + '{"encryptionType":"xor", "encryptionKey":"secret", + "debugEncryptionKeyText":1 + }' + ); + } +} -test { + faultsim_test_result {0 {{}}} +} + +set sql { + SELECT zonefile_write('test.zonefile', 'tt', + '{"compressionTypeContent":"zstd_global_dict"}' + ); +} +set HAVE_ZSTD 0 +if {[catch { db eval $sql }]==0} { + set HAVE_ZSTD 1 +} + +if {$HAVE_ZSTD} { + do_faultsim_test 1.2 -faults oom* -prep { + sqlite3 db test.db + load_static_extension db zonefile + } -body { + execsql { + SELECT zonefile_write('test.zonefile', 'tt', + '{"compressionTypeContent":"zstd_global_dict"}' + ); + } + } -test { + faultsim_test_result {0 {{}}} + } +} + +do_execsql_test 1.3.0 { UPDATE tt SET frame = NULL; } +do_faultsim_test 1.3 -faults oom* -prep { + sqlite3 db test.db + load_static_extension db zonefile +} -body { + execsql { + SELECT zonefile_write('test.zonefile', 'tt'); + } +} -test { + faultsim_test_result {0 {{}}} +} + +do_execsql_test 1.4.0 { + INSERT INTO tt VALUES(5, -1, -1, randomblob(100)); + INSERT INTO tt VALUES(6, -1, -1, randomblob(100)); + INSERT INTO tt VALUES(7, -1, -1, randomblob(100)); + INSERT INTO tt VALUES(8, -1, -1, randomblob(100)); + INSERT INTO tt VALUES(9, -1, -1, randomblob(100)); + CREATE VIRTUAL TABLE ttz USING zonefile; +} +if {$HAVE_ZSTD} { + faultsim_save_and_close + do_faultsim_test 1.4 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile + } -body { + execsql { + SELECT zonefile_write('test.zonefile', 'tt', + '{"compressionTypeIndexData":"zstd"}' + ); + } + } -test { + faultsim_test_result {0 {{}}} + } + + faultsim_save_and_close + do_faultsim_test 1.5 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile + } -body { + execsql { + INSERT INTO ttz_files(filename) VALUES('test.zonefile'); + } + } -test { + faultsim_test_result {0 {}} + } +} + +#------------------------------------------------------------------------- +# +do_execsql_test 2.0 { + SELECT zonefile_write('test.zonefile', 'tt', + '{"encryptionType":"xor", "encryptionKey":"secret", + "debugEncryptionKeyText":1 + }' + ); + CREATE VIRTUAL TABLE zz USING zonefile; +} {{}} + +faultsim_save_and_close +do_faultsim_test 2.1 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile +} -body { + execsql { + INSERT INTO zz_files(filename, ekey) VALUES('test.zonefile', 'secret') + } +} -test { + faultsim_test_result {0 {}} +} + +faultsim_save_and_close +do_faultsim_test 2.2 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile +} -body { + execsql { SELECT json_extract(header, '$.magicNumber') FROM zz_files } +} -test { + faultsim_test_result {0 1179332920} +} + +do_faultsim_test 2.3 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile +} -body { + execsql { DELETE FROM zz_files } +} -test { + faultsim_test_result {0 {}} +} + +#------------------------------------------------------------------------- +# +reset_db +faultsim_save_and_close +do_faultsim_test 3.1 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile +} -body { + execsql { CREATE VIRTUAL TABLE t1 USING zonefile(cachesize=5) } +} -test { + faultsim_test_result {0 {}} +} + +faultsim_save_and_close +do_faultsim_test 3.2 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile +} -body { + execsql { DROP TABLE t1 } +} -test { + faultsim_test_result {0 {}} +} + +#------------------------------------------------------------------------- +# +reset_db +load_static_extension db zonefile +do_execsql_test 4.0 { + CREATE TABLE zz(k INTEGER PRIMARY KEY, frame INTEGER, idx INTEGER, v BLOB); + INSERT INTO zz VALUES(1, -1, -1, randomblob(100)); + INSERT INTO zz VALUES(2, -1, -1, randomblob(100)); + INSERT INTO zz VALUES(3, -1, -1, randomblob(100)); + SELECT zonefile_write('test.zonefile', 'zz'); + CREATE VIRTUAL TABLE zone USING zonefile; + INSERT INTO zone_files(filename) VALUES('test.zonefile'); +} {{}} + +faultsim_save_and_close +do_faultsim_test 4.1 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile +} -body { + execsql { SELECT v IS NULL FROM zone WHERE k = 2 } +} -test { + faultsim_test_result {0 0} +} + + +if {$HAVE_ZSTD} { + set params { + {"encryptionType":"xor","encryptionKey":"pass", + "compressionTypeContent":"zstd_global_dict", + "debugEncryptionKeyText":1 + } + } +} else { + set params { + {"encryptionType":"xor","encryptionKey":"pass", + "debugEncryptionKeyText":1 + } + } +} +do_execsql_test 4.2 { + SELECT zonefile_write('test.zonefile', 'zz', $params); + CREATE VIRTUAL TABLE zone2 USING zonefile; + INSERT INTO zone2_files(filename,ekey) VALUES('test.zonefile','pass'); +} {{}} + +faultsim_save_and_close +do_faultsim_test 4.3 -faults oom* -prep { + faultsim_restore_and_reopen + load_static_extension db zonefile + execsql { UPDATE zone2_files SET ekey='pass' } +} -body { + execsql { SELECT v IS NULL FROM zone2 WHERE k = 2 } +} -test { + faultsim_test_result {0 0} +} + +#------------------------------------------------------------------------- +reset_db +do_faultsim_test 4.4 -faults oom* -prep { + faultsim_restore_and_reopen +} -body { + load_static_extension db zonefile +} -test { + faultsim_test_result {0 {}} {1 {initialization of zonefile failed: }} +} + +finish_test + Index: main.mk ================================================================== --- main.mk +++ main.mk @@ -370,10 +370,11 @@ $(TOP)/ext/misc/totype.c \ $(TOP)/ext/misc/unionvtab.c \ $(TOP)/ext/misc/wholenumber.c \ $(TOP)/ext/misc/vfslog.c \ $(TOP)/ext/misc/zipfile.c \ + $(TOP)/ext/zonefile/zonefile.c \ $(TOP)/ext/fts5/fts5_tcl.c \ $(TOP)/ext/fts5/fts5_test_mi.c \ $(TOP)/ext/fts5/fts5_test_tok.c Index: src/test1.c ================================================================== --- src/test1.c +++ src/test1.c @@ -6990,10 +6990,11 @@ extern int sqlite3_wholenumber_init(sqlite3*,char**,const sqlite3_api_routines*); extern int sqlite3_unionvtab_init(sqlite3*,char**,const sqlite3_api_routines*); #ifdef SQLITE_HAVE_ZLIB extern int sqlite3_zipfile_init(sqlite3*,char**,const sqlite3_api_routines*); #endif + extern int sqlite3_zonefile_init(sqlite3*,char**,const sqlite3_api_routines*); static const struct { const char *zExtName; int (*pInit)(sqlite3*,char**,const sqlite3_api_routines*); } aExtension[] = { { "amatch", sqlite3_amatch_init }, @@ -7014,10 +7015,11 @@ { "unionvtab", sqlite3_unionvtab_init }, { "wholenumber", sqlite3_wholenumber_init }, #ifdef SQLITE_HAVE_ZLIB { "zipfile", sqlite3_zipfile_init }, #endif + { "zonefile", sqlite3_zonefile_init }, }; sqlite3 *db; const char *zName; int i, j, rc; char *zErrMsg = 0;