ForestEXOR Games Blog

引退プログラマーは最新技術に着いて行けない

SQLite使ってみる その4 vfsを使ったアーカイブ読み込み編

前回の
SQLite使ってみる その3 vfsを使った暗号化編 - forestexor’s blog(仮)
にてvfsで自分でsqliteのdbファイルを作成することができたので今回はアーカイブファイルを読み込んでその中のdbファイルの情報を取り出す方法を書いていきたいと思います。


・逃げ口上
このプログラムによって引き起こされた一切の責任を負いません、自己責任でお願いします。



まず、基本的な所は前回とほとんど同じです。
今回はアーカイブファイル内から、ということで書き込み処理は絶対NGなのでその部分をエラー処理にすること、あとはオープン時の処理が違うということくらいです。

必要なヘッダー

自分の処理が悪いだけでもっと減らせるかも。

#include <stdio.h>
#include <crtdbg.h>
#include "sqlite3.h"
#pragma comment( lib, "sqlite3-x86.dll" )

#include <memory>
#include <string>
#include <fstream>
#include <iostream>
#include <iterator>
#include <sstream>
#include <unordered_map>
#include <algorithm>
独自実装構造体

前回は暗号保存や書き込み処理もあったのでfstreamを使いました。
今回は読み込む処理だけなのでstringstreamを使います。

struct SQLiteFile : sqlite3_file{
	std::shared_ptr<std::stringstream>	pStream;
	int					iLockLevel;
};
xClose

暗号化保存の必要がないのでメモリを開放するだけ、shared_ptrなのでぶっちゃけ何もする必要は無いと思いますがメモリ管理はきちんと意識しましょう。(老害的主張)

int xClose( sqlite3_file* pFileBase ){
	::printf_s("xClose\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->pStream = nullptr;
	return SQLITE_OK;
}
xRead

変更点無し

int xRead( sqlite3_file* pFileBase, void* buf, int iAmt, sqlite3_int64 iOfst ){
	::printf_s("xRead\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	pFile->pStream->sync();
	pFile->pStream->seekg( iOfst, std::ios::beg );
	if( pFile->pStream->fail() ){
		pFile->pStream->clear();
		memset( static_cast<char*>(buf), 0, iAmt );
		return SQLITE_IOERR_SHORT_READ;
	}

	pFile->pStream->read( static_cast<char*>(buf), iAmt );

	const auto gcount = pFile->pStream->gcount();
	if( gcount < iAmt ){
		memset( static_cast<char*>(buf) + gcount, 0, static_cast<size_t>(iAmt - gcount) );
		return SQLITE_IOERR_SHORT_READ;
	}

	return SQLITE_OK;
}
xWrite

アーカイブに書き込むととてもやばいので禁止。 一応、ファイルからstringstreamに移しているので書き込み処理を実装しても問題はありません。

int xWrite( sqlite3_file* pFileBase, const void* buf, int iAmt, sqlite3_int64 iOfst ){
	::printf_s("xWrite\n");
	return SQLITE_READONLY;
}
xTruncate

変更点無し。 こんな事プログラマーが言ったら絶対駄目なんですけどこのメソッドが何をしたいのか解ってないです、不要そうだったので調べてすらない。

int xTruncate( sqlite3_file* pFileBase, sqlite3_int64 size ){
	::printf_s("xTruncate\n");
	return SQLITE_OK;
}
xSync

変更点無し。

int xSync( sqlite3_file* pFileBase, int flags ){
	::printf_s("xSync\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	return pFile->pStream->sync();
}
xFileSize

変更点無し。

int xFileSize( sqlite3_file* pFileBase, sqlite3_int64* pSize ){
	::printf_s("xFileSize\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	(*pSize) = pFile->pStream->seekg( 0, std::ios::end ).tellg();

	if( pFile->pStream->fail() ){
		pFile->pStream->clear();
	}

	return SQLITE_OK;
}
xLock

変更点無し。

int xLock( sqlite3_file* pFileBase, int iLevel ){
	::printf_s("xLock\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->iLockLevel = iLevel;
	return SQLITE_OK;
}
xUnlock

変更点無し。

int xUnlock( sqlite3_file* pFileBase, int iLevel ){
	::printf_s("xUnlock\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->iLockLevel = iLevel;
	return SQLITE_OK;
}
xCheckReservedLock

変更点無し。

int xCheckReservedLock( sqlite3_file* pFileBase, int* pResOut ){
	::printf_s("xCheckReservedLock\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	(*pResOut) = (pFile->iLockLevel >= 1);
	return SQLITE_OK;
}
xFileControl

変更点無し。

int xFileControl( sqlite3_file* pFileBase, int op, void* pArg ){
	::printf_s("xFileControl\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	switch(op){
	case SQLITE_FCNTL_LOCKSTATE:
		(*reinterpret_cast<int*>(pArg)) = pFile->iLockLevel;
		break;
	case SQLITE_FCNTL_SIZE_HINT:
		break;
	case SQLITE_FCNTL_CHUNK_SIZE:
		break;
	case SQLITE_GET_LOCKPROXYFILE:	return SQLITE_ERROR;
	case SQLITE_SET_LOCKPROXYFILE:	return SQLITE_ERROR;
	case SQLITE_LAST_ERRNO:			return SQLITE_ERROR;
	}

	return SQLITE_OK;
}
xSectorSize

変更点無し。

int xSectorSize( sqlite3_file* ){
	::printf_s("xSectorSize\n");
	return 512;
}
xDeviceCharacteristics

変更点無し。

int xDeviceCharacteristics( sqlite3_file* ){
	::printf_s("xDeviceCharacteristics\n");
	return (SQLITE_IOCAP_ATOMIC | SQLITE_IOCAP_SAFE_APPEND | SQLITE_IOCAP_SEQUENTIAL);
}
xOpen

ここが本稿のメイン部分です。
学生時代は素材剥き出しで読み込みだったし、会社では既に会社のブラックボックスなライブラリで読み込みだったので、アーカイブファイルを作るのも読み込むのも初めてやりました。 色々と至らない処理だと思いますがあくまでも例という事でご容赦願います。

アーカイブファイルのヘッダー部分はこんな感じで作ってあります。
1行目はファイル数
以降はファイル名、アーカイブ内の位置、ファイルサイズです。

3
01.png,0,9484
test.db,9484,8192
02.png,17676,10825

その後にpngファイルとdbファイルとpngファイルをそのままくっつけただけの物です。
ちなみにアーカイブファイルってヘッダー部分さえ暗号化していれば大丈夫って何処かで見た記憶があります、ソースは無い。 そして今回は主題じゃないので暗号化もしてません。

int xOpen( sqlite3_vfs* pVFS, const char* zName, sqlite3_file* pFileBase, int flags, int* pOutFlags ){
	::printf_s("xOpen\n");

	// 読み取り専用フラグじゃないならエラー
	if( 0 == (flags & SQLITE_OPEN_READONLY) ){
		return SQLITE_READONLY;
	}

	static sqlite3_io_methods methods;
	methods.iVersion = 1;
	methods.xClose = &xClose;
	methods.xRead = &xRead;
	methods.xWrite = &xWrite;
	methods.xTruncate = &xTruncate;
	methods.xSync = &xSync;
	methods.xFileSize = &xFileSize;
	methods.xLock = &xLock;
	methods.xUnlock = &xUnlock;
	methods.xCheckReservedLock = &xCheckReservedLock;
	methods.xFileControl = &xFileControl;
	methods.xSectorSize = &xSectorSize;
	methods.xDeviceCharacteristics = &xDeviceCharacteristics;
	pFileBase->pMethods = &methods;

	if( nullptr != pOutFlags ){
		(*pOutFlags) = flags;
	}

	// アーカイブファイルのヘッダー読み取り用
	struct FileInfo{
		std::streamoff FilePos;
		std::streamoff FileSize;
	};

	// アーカイブファイル読み込み(読み込むファイル名は直接指定するしかない)
	std::ifstream	ifs( "test.dat", std::fstream::binary ); 
	if( false == ifs.is_open() ){
		::printf_s("can not open test.dat\n");
		return SQLITE_ERROR;
	}

	// 1行目
	std::string str;
	std::getline( ifs, str );

	// アーカイブされているファイル数を取得
	size_t FileNum = ::atoi( str.data() );
	if( 0 == FileNum ){
		::printf_s("FileNum = 0\n");
		return SQLITE_ERROR;
	}

	// ファイル数分1行ずつヘッダーを読み込む
	std::unordered_map<std::string,FileInfo> FileInfoMap;
	for( size_t i(0) ; i < FileNum ; i++ ){
		std::stringstream ss;
		std::getline( ifs, str );
		std::replace( str.begin(), str.end(), ',', ' ' );
		ss << str.data();
		FileInfo info;
		str = ss.str();
		ss >> str;
		ss >> info.FilePos;
		ss >> info.FileSize;
		FileInfoMap.emplace( std::make_pair( str, info ) );
	}
	// ヘッダーサイズ取得
	std::streamoff HeaderSize = ifs.tellg();

	// sqlite3_open_v2でファイル名だけを指定しても自動で絶対パスになる
	// のでファイル名(+拡張子)だけに再リネーム
	str = zName;
	str = str.substr( str.rfind("\\")+1 );
	// test.db部分のヘッダー情報を取得
	FileInfo info = FileInfoMap[str];

	// アーカイブからtest.db部分だけのバッファを読み込む
	str.clear();
	str.resize( (size_t)info.FileSize );
	ifs.clear();
	ifs.seekg( (info.FilePos + HeaderSize), std::fstream::beg );
	ifs.read( &str[0], info.FileSize );
	ifs.close();

	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->pStream = std::shared_ptr<std::stringstream>( new std::stringstream() );
	pFile->iLockLevel = 0;

	// 読み込んだバッファを流す(ifstreamから直接stringstreamに渡す方法もあるかも)
	pFile->pStream->write( str.data(), str.size() );

	return SQLITE_OK;
}
main

以降はきちんと動いているかの検証です、細かいコメントはその3の方が詳しく書いてあります。

int main(){
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif

	int iErr = 0;

	// vfs作成
	sqlite3_vfs* pVFS = nullptr;
	pVFS = sqlite3_vfs_find( "win32" );
	sqlite3_vfs vfs;
	memset( &vfs, 0, sizeof(sqlite3_vfs) );
	vfs.iVersion = 3;
	vfs.szOsFile = sizeof(SQLiteFile);
	vfs.mxPathname = pVFS->mxPathname;
	vfs.zName = "myVFS";
	vfs.pAppData = pVFS->pAppData;
	vfs.xOpen = &xOpen;
	vfs.xDelete = pVFS->xDelete;
	vfs.xAccess = pVFS->xAccess;
	vfs.xFullPathname = pVFS->xFullPathname;
	vfs.xDlOpen = pVFS->xDlOpen;
	vfs.xDlError = pVFS->xDlError;
	vfs.xDlSym = pVFS->xDlSym;
	vfs.xDlClose = pVFS->xDlClose;
	vfs.xRandomness = pVFS->xRandomness;
	vfs.xSleep = pVFS->xSleep;
	vfs.xCurrentTime = pVFS->xCurrentTime;
	vfs.xGetLastError = pVFS->xGetLastError;
	vfs.xCurrentTimeInt64 = pVFS->xCurrentTimeInt64;

	// vfs登録
	::printf_s( "sqlite3_vfs_register\n" );
	iErr = sqlite3_vfs_register( &vfs, 0 );
	if( SQLITE_OK != iErr && SQLITE_CANTOPEN != iErr ){
		::printf_s( "vfs register failed.\n" );
		::getchar();
		return -1;
	}

	char* strDBName = "test.db";
	char* strErrMsg = nullptr;
	sqlite3* pDB = nullptr;

	// 読み取り専用モードで開く
	::printf_s( "sqlite3_open_v2\n" );
	iErr = ::sqlite3_open_v2( strDBName, &pDB, SQLITE_OPEN_READONLY, "myVFS" );
	if( SQLITE_OK != iErr ){
		// オープン失敗
		::printf_s( "DB open failed.\n%s\n", ::sqlite3_errmsg(pDB) );

		::printf_s( "sqlite3_close\n" );
		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );
		
	}else{
		// 既存の.dbファイルの読み込みに成功
		::printf_s( "DB open success.\n" );
	}

	// レコードの抽出と表示
	sqlite3_stmt* stmt = nullptr;	// ステートメント
	::printf_s( "sqlite3_prepare_v2 select\n" );
	iErr = ::sqlite3_prepare_v2( pDB, "select * from hoge", -1, &stmt, nullptr );
	if( SQLITE_OK != iErr ){
		// 抽出失敗
		::printf_s( "%s\n", ::sqlite3_errmsg(pDB) );
		::sqlite3_reset( stmt );
		::sqlite3_finalize( stmt );

		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );
		::getchar();
		return -1;

	}else{
		// 抽出成功
		::printf_s( "select success.\n" );
		for( ; SQLITE_ROW == ::sqlite3_step(stmt) ; ){	// step1回で1レコード取得
			::printf_s("+--------+-----------------+\n| ");
			// レコードの列数取得
			int iColMax = ::sqlite3_column_count(stmt);
			for( int i(0) ; i < iColMax ; i++ ){
				// 列名取得
				const char* name = sqlite3_column_name( stmt, i );
				// 列のデータ型取得
				int iColType = sqlite3_column_type( stmt, i );

				switch( iColType ){
				// INTEGER(int)
				case SQLITE_INTEGER:
					::printf_s( "%s = %d", name, sqlite3_column_int( stmt, i ) );
					break;
				// REAL(double)
				case SQLITE_FLOAT:
					::printf_s( "%s = %f", name, (float)sqlite3_column_double( stmt, i ) );
					break;
				// TEXT(string)
				case SQLITE_TEXT:
					::printf_s( "%s = %s", name, sqlite3_column_text( stmt, i ) );
					break;
				// NULL(nullptr)
				case SQLITE_NULL:
					break;
				// BLOB(不明)
				case SQLITE_BLOB:
					break;
				}

				::printf_s(" | ");
			}// for( int i(0) ; i < iColMax ; i++ )
			::printf_s("\n");
		}// for( ; SQLITE_ROW == ::sqlite3_step(stmt) ; )
		::printf_s("+--------+-----------------+\n");
	}
	// stmtの後始末
	::sqlite3_reset( stmt );
	::sqlite3_finalize( stmt );

	// おまけ2 テーブル名の取得
	::printf_s( "sqlite3_prepare_v2 select\n" );
	iErr = sqlite3_prepare_v2( pDB, "select tbl_name from sqlite_master where type='table'", -1, &stmt, nullptr );
	if( SQLITE_OK != iErr ){
		::printf_s( "%s\n", ::sqlite3_errmsg(pDB) );
		::sqlite3_reset( stmt );
		::sqlite3_finalize( stmt );
		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );
		::getchar();
		return -1;
	}else{
		// 表示
		::printf_s( "select success\n" );
		while( SQLITE_ROW == sqlite3_step(stmt) ){
			int iMax = sqlite3_column_count(stmt);
			for( int i(0) ; i < iMax ; i++ ){
				const char* name = sqlite3_column_name( stmt, i );
				int iColType = sqlite3_column_type( stmt, i );
				if( SQLITE_TEXT == iColType ){
					::printf_s( "%s = %s\n", name, sqlite3_column_text( stmt, i ) );
				}else if( SQLITE_INTEGER == iColType ){
					::printf_s( "%s = %d\n", name, sqlite3_column_int( stmt, i ) );
				}
			}
		}
	}
	::sqlite3_reset( stmt );
	::sqlite3_finalize( stmt );

	// 後始末
	::printf_s( "sqlite3_close\n" );
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );

	::getchar();
	return 0;
}