You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

523 lines
15 KiB
C

// Run an sql command without any input. Useful if you want to use wine debugger
// where it's impossible to pipe in a file/program to stdin.
// #define SINGLE_COMMAND L"select TABLE_NAME from INFORMATION_SCHEMA.TABLES;"
// #define SINGLE_COMMAND L"select * from GameData;"
#define __USE_MINGW_ANSI_STDIO 0
#include <stdio.h>
#include <wchar.h>
#include <stdbool.h>
#include <stdint.h>
// setmode
#include <fcntl.h>
#include <io.h>
#define DBINITCONSTANTS
#define WIN32_LEAN_AND_MEAN
#ifndef UNICODE
#define UNICODE
#endif
#include <windows.h>
#include <objbase.h>
#include <guiddef.h>
#include <oledb.h>
extern BOOLEAN NTAPI SystemFunction036(PVOID, ULONG);
static const GUID CLSID_SQLSERVERCE_3_5 =
{0xf49c559d, 0xe9e5, 0x467c, {0x8c, 0x18, 0x33, 0x26, 0xaa, 0xe4, 0xeb, 0xcc}};
static const GUID CLSID_Engine =
{0xa9d3060d, 0x3526, 0x4538, {0xb1, 0x3a, 0x19, 0x13, 0x56, 0x8d, 0xaa, 0x0d}};
static const GUID IID_ISSCEEngine =
{0x10ec3e45,0x0870,0x4d7b, {0x9a,0x2d,0xf4,0xf8,0x1b,0x6b,0x7f,0xa2}};
static const GUID DBPROPSET_SSCE_DBINIT =
{0x2b9ab5ba, 0x4f6c, 0x4ddd, {0xbf, 0x18, 0x24, 0xdd, 0x4b, 0xd4, 0x18, 0x48}};
#define DBPROP_SSCE_DBPASSWORD 0x1FBL
#define DBPROP_SSCE_MAX_DATABASE_SIZE 0x20BL
#ifdef __WINE_WINDOWS_H
static const DBID DB_NULLID = {{{0x00000000L,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}}}, 0, {(LPOLESTR) NULL}};
# ifndef __IAccessor_INTERFACE_DEFINED__
enum DBACCESSORFLAGSENUM
{
DBACCESSOR_INVALID = 0,
DBACCESSOR_PASSBYREF = 0x1,
DBACCESSOR_ROWDATA = 0x2,
DBACCESSOR_PARAMETERDATA = 0x4,
DBACCESSOR_OPTIMIZED = 0x8,
DBACCESSOR_INHERITED = 0x10
};
# endif
#endif
typedef interface ISSCEEngine ISSCEEngine;
interface ISSCEEngine
{
CONST_VTBL struct ISSCEEngineVtbl *lpVtbl;
};
typedef enum REPAIROPTION { DELETECORRUPTED, RECOVERCORRUPTED } REPAIROPTION;
typedef struct ISSCEEngineVtbl
{
BEGIN_INTERFACE
HRESULT (STDMETHODCALLTYPE *QueryInterface)(ISSCEEngine* thiz, REFIID riid, void **obj);
ULONG (STDMETHODCALLTYPE *AddRef)(ISSCEEngine* This);
ULONG (STDMETHODCALLTYPE *Release)(ISSCEEngine* This);
HRESULT (STDMETHODCALLTYPE *CompactDatabase)(
ISSCEEngine* thiz, BSTR src_conn, BSTR dst_conn);
HRESULT (STDMETHODCALLTYPE *get_ErrorRecords)(
ISSCEEngine* thiz, /*ISSCEErrors*/ void **val);
HRESULT (STDMETHODCALLTYPE *CreateDatabase)(ISSCEEngine* thiz, BSTR conn);
HRESULT (STDMETHODCALLTYPE *Repair)(
ISSCEEngine* thiz, BSTR src, BSTR dst, REPAIROPTION opt);
// doesn't do anything on wine?
HRESULT (STDMETHODCALLTYPE *UpgradeDatabase)(
ISSCEEngine* thiz, BSTR src, BSTR dst);
END_INTERFACE
} ISSCEEngineVtbl;
#define DB_PW \
L"\u00C7\u00B0\u00A5\u00E0\u00F2\u00C5\u00C5\u00C7\u00C9\u00E0\u00F1\u00F1\u00F8"
#define COM(x, fun, ...) ((x)->lpVtbl->fun((x), ##__VA_ARGS__))
#define ALEN(x) (sizeof(x) / sizeof(x[0]))
#define EXIT_OK 0
#define EXIT_USER 1 // invalid cmd line usage
#define EXIT_COMM 2 // read invalid data on stdin
#define EXIT_UNREC 3 // unrecoverable error
static void check(HRESULT hr, const char* file, int line)
{
if (!FAILED(hr)) return;
IErrorInfo* ei;
GetErrorInfo(0, &ei);
if (ei != NULL)
{
BSTR str;
COM(ei, GetDescription, &str);
fprintf(stderr, "%s:%d: HRESULT %x: %ls\n", file, line, (unsigned) hr, str);
SysFreeString(str);
COM(ei, Release);
}
else
fprintf(stderr, "%s:%d: HRESULT %x\n", file, line, (unsigned) hr);
fflush(stdout);
fflush(stderr);
exit(EXIT_UNREC);
}
#define CHECK(x) check((x), __FILE__, __LINE__)
static void win_err(const char* file, int line)
{
fprintf(stderr, "%s:%d: Win Error %d\n", file, line, (int) GetLastError());
fflush(stdout);
fflush(stderr);
exit(EXIT_UNREC);
}
#define WIN_ERR() win_err(__FILE__, __LINE__)
typedef BOOL (WINAPI* dll_get_class_object_t)(REFCLSID, REFIID, LPVOID*);
static void convert_db(const wchar_t* path_from, const wchar_t* path_to)
{
HMODULE hm = LoadLibraryA("sqlceca35.dll");
if (hm == NULL) WIN_ERR();
dll_get_class_object_t proc = (dll_get_class_object_t)
GetProcAddress(hm, "DllGetClassObject");
if (proc == NULL) WIN_ERR();
IClassFactory* factory;
CHECK(proc(&CLSID_Engine, &IID_IClassFactory, (void**) &factory));
ISSCEEngine* engine;
CHECK(COM(factory, CreateInstance, NULL, &IID_ISSCEEngine, (void**) &engine));
COM(factory, Release);
wchar_t ci[4096];
wcscpy(ci+2, L"DataSource=");
wcscat(ci+2, path_from);
wcscat(ci+2, L";Max Database Size=4024;Password=" DB_PW);
DWORD len = 2*wcslen(ci+2);
memcpy(ci, &len, sizeof(len));
wchar_t co[4096];
wcscpy(co+2, L"DataSource=");
wcscat(co+2, path_to);
wcscat(co+2, L";Max Database Size=4024");
len = 2*wcslen(co+2);
memcpy(co, &len, sizeof(len));
// UpgradeDatabase doesn't seem to do anything
// CompactDatabase and Repair apparently does the same, but Repair creates
// a logfile (to chage_ext(data_source, ".log"))
CHECK(COM(engine, CompactDatabase, ci+2, co+2));
COM(engine, Release);
FreeLibrary(hm);
}
static IDBInitialize* init_oledb(void)
{
// shamelessly leak HMODULE, but only load once
static HMODULE hm = NULL;
if (hm == NULL) hm = LoadLibraryA("sqlceoledb35.dll");
if (hm == NULL) WIN_ERR();
dll_get_class_object_t proc = (dll_get_class_object_t)
GetProcAddress(hm, "DllGetClassObject");
if (proc == NULL) WIN_ERR();
IClassFactory* factory;
CHECK(proc(&CLSID_SQLSERVERCE_3_5, &IID_IClassFactory, (void**) &factory));
IDBInitialize* init;
CHECK(COM(factory, CreateInstance, NULL, &IID_IDBInitialize, (void**) &init));
COM(factory, Release);
return init;
}
static IDBCreateCommand* open_db(IDBInitialize* init, const wchar_t* fname)
{
IDBProperties* props;
CHECK(COM(init, QueryInterface, &IID_IDBProperties, (void**) &props));
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr
BSTR fname_dup = SysAllocString(fname);
// RAGS connection string:
// "DataSource=foo.rag;Max Database Size=4024;Password=<that garbage above>"
DBPROP common_props[] = {
{ DBPROP_INIT_DATASOURCE, DBPROPOPTIONS_REQUIRED, 0, DB_NULLID,
{{{VT_BSTR, 0,0,0, {.bstrVal = fname_dup}}}} },
};
DBPROP sqlce_props[] = {
/* { DBPROP_SSCE_DBPASSWORD, DBPROPOPTIONS_REQUIRED, 0, DB_NULLID, */
/* {{{VT_BSTR, 0,0,0, {.bstrVal = DB_PW}}}} }, */
{ DBPROP_SSCE_MAX_DATABASE_SIZE, DBPROPOPTIONS_REQUIRED, 0, DB_NULLID,
{{{VT_I4, 0,0,0, {.intVal = 4024}}}} },
};
DBPROPSET propsets[] = {
{common_props, ALEN(common_props), DBPROPSET_DBINIT},
{sqlce_props, ALEN(sqlce_props), DBPROPSET_SSCE_DBINIT}
};
CHECK(COM(props, SetProperties, ALEN(propsets), propsets));
SysFreeString(fname_dup);
COM(props, Release);
CHECK(COM(init, Initialize));
IDBCreateSession* sess;
CHECK(COM(init, QueryInterface, &IID_IDBCreateSession, (void**) &sess));
IDBCreateCommand* creat;
CHECK(COM(sess, CreateSession, NULL, &IID_IDBCreateCommand,
(IUnknown**) &creat));
COM(sess, Release);
return creat;
}
_Static_assert(sizeof(uint8_t) == 1, "wtf?");
_Static_assert(sizeof(DWORD) == sizeof(uint32_t), "boom");
_Static_assert(sizeof(wchar_t) == 2, "wchar_t is not windows like");
typedef struct
{
uint32_t length;
wchar_t data[];
} bstr_like;
#define GEN_SEND(suf, type) \
static void send_##suf(type d) \
{ \
if (fwrite(&d, sizeof(type), 1, stdout) != 1) exit(EXIT_UNREC); \
}
GEN_SEND(u8, uint8_t)
GEN_SEND(u32, uint32_t)
static void send_wstring(const wchar_t* data, uint32_t len)
{
if (len == (uint32_t) -1) len = wcslen(data);
len *= 2;
if (fwrite(&len, sizeof(uint32_t), 1, stdout) != 1) exit(EXIT_UNREC);
if (len && fwrite(data, len, 1, stdout) != 1) exit(EXIT_UNREC);
}
static void send_string(const char* data, uint32_t len)
{
if (len == (uint32_t) -1) len = strlen(data);
if (fwrite(&len, sizeof(uint32_t), 1, stdout) != 1) exit(EXIT_UNREC);
if (len && fwrite(data, len, 1, stdout) != 1) exit(140+EXIT_UNREC);
}
static bool send_hresult(HRESULT hr)
{
if (!FAILED(hr)) return false;
send_u8(0xff);
send_u32(hr);
IErrorInfo* ei;
GetErrorInfo(0, &ei);
if (ei != NULL)
{
BSTR str;
COM(ei, GetDescription, &str);
if (str)
{
send_wstring(str, SysStringLen(str));
SysFreeString(str);
}
else send_wstring(L"", 0);
COM(ei, Release);
}
else
send_wstring(L"", 0);
return true;
}
#define CRET(hr) do if (send_hresult(hr)) goto end; while (0)
static void send_cerror(const char* fmt)
{
send_u8(0xfe);
send_string(fmt, strlen(fmt));
}
// read a string (either char or wchar, doesn't matter...)
static bstr_like* read_string(void)
{
uint32_t l;
if (fread(&l, sizeof(uint32_t), 1, stdin) != 1) exit(EXIT_COMM);
bstr_like* ret = (bstr_like*) malloc(sizeof(bstr_like) + l + 2);
if (ret == NULL) return NULL;
ret->length = l;
if (fread(&ret->data, l, 1, stdin) != 1) { free(ret); return NULL; }
memset(((char*) ret->data) + l, 0, 2); // actually doesn't have to be wchar
return ret;
}
typedef struct
{
DBBINDING bind;
HACCESSOR accessor;
bool isblob;
} col_data;
static void cmd_exec(IDBCreateCommand* creat)
{
#ifdef SINGLE_COMMAND
bstr_like* cmd = malloc(sizeof(bstr_like) + (wcslen(SINGLE_COMMAND) + 1) * sizeof(DWORD));
cmd->length = 2*wcslen(SINGLE_COMMAND);
wcscpy(cmd->data, SINGLE_COMMAND);
#else
bstr_like* cmd = read_string();
if (cmd == NULL) { send_cerror("bad alloc"); return; }
#endif
ICommandText* ct = NULL;
IRowset* rowset = NULL;
IColumnsInfo* cii = NULL;
DBCOLUMNINFO* ci = NULL;
col_data* cd = NULL;
IAccessor* accessori = NULL;
DWORD* row_data = NULL;
CRET(COM(creat, CreateCommand, NULL, &IID_ICommandText, (IUnknown**) &ct));
CRET(COM(ct, SetCommandText, &DBGUID_DEFAULT, cmd->data));
free(cmd); cmd = NULL;
CRET(COM(ct, Execute, NULL, &IID_IRowset, NULL, NULL, (IUnknown**) &rowset));
if (rowset == NULL)
{
send_u8(0);
goto end;
}
CRET(COM(rowset, QueryInterface, &IID_IColumnsInfo, (void**)&cii));
DBORDINAL n_cols;
OLECHAR* buf;
CRET(COM(cii, GetColumnInfo, &n_cols, &ci, &buf));
cd = (col_data*) calloc(n_cols, sizeof(col_data));
if (cd == NULL) { send_cerror("bad alloc"); goto end; }
DBOBJECT blob = { STGM_READ, IID_ISequentialStream };
CRET(COM(rowset, QueryInterface, &IID_IAccessor, (void**) &accessori));
size_t max_size = sizeof(IUnknown*);
for (DBORDINAL i = 0; i < n_cols; ++i)
{
cd[i].isblob = ci[i].dwFlags & DBCOLUMNFLAGS_ISLONG;
cd[i].bind.iOrdinal = ci[i].iOrdinal;
cd[i].bind.obValue = 2 * sizeof(DWORD);
cd[i].bind.obLength = 0;
cd[i].bind.obStatus = sizeof(DWORD);
cd[i].bind.pTypeInfo = NULL;
cd[i].bind.pObject = cd[i].isblob ? &blob : NULL;
cd[i].bind.pBindExt = NULL;
cd[i].bind.dwPart = DBPART_VALUE | DBPART_LENGTH | DBPART_STATUS;
cd[i].bind.dwMemOwner = DBMEMOWNER_CLIENTOWNED;
// cd[i].bind.eParamIO ignored
cd[i].bind.cbMaxLen = cd[i].isblob ? 0 : ci[i].ulColumnSize;
cd[i].bind.dwFlags = 0;
cd[i].bind.wType = cd[i].isblob ? DBTYPE_IUNKNOWN : ci[i].wType;
cd[i].bind.bPrecision = ci[i].bPrecision;
cd[i].bind.bScale = ci[i].bScale;
if (!cd[i].isblob && ci[i].ulColumnSize > max_size)
max_size = ci[i].ulColumnSize;
CRET(COM(accessori, CreateAccessor, DBACCESSOR_ROWDATA, 1, &cd[i].bind,
0, &cd[i].accessor, NULL));
}
send_u8(1);
send_u32(n_cols);
for (DBORDINAL i = 0; i < n_cols; ++i)
{
send_u32(ci[i].wType);
send_u32(ci[i].dwFlags);
send_wstring(ci[i].pwszName, -1);
}
row_data = malloc(2 * sizeof(DWORD) + max_size + 65535);
if (row_data == NULL) { send_cerror("bad alloc"); goto end; }
HROW* row = NULL;
DBCOUNTITEM got_rows;
while (COM(rowset, GetNextRows, DB_NULL_HCHAPTER, 0, 1, &got_rows, &row) == S_OK)
{
if (got_rows != 1) { send_cerror("invalid got rows"); goto end; }
send_u8(1);
for (DBORDINAL i = 0; i < n_cols; ++i)
{
CRET(COM(rowset, GetData, *row, cd[i].accessor, row_data));
DWORD len = row_data[0];
DWORD status = row_data[1];
if (status == DBSTATUS_S_ISNULL) len = 0;
send_u8(cd[i].isblob ? 1 : 0);
send_u32(status);
if (cd[i].isblob)
{
ISequentialStream* is = *(ISequentialStream**) &row_data[2];
if (is)
{
char buf[4096];
ULONG act_read;
do
{
CRET(COM(is, Read, buf, ALEN(buf), &act_read));
if (act_read == 0) break;
send_u8(1);
send_string(buf, act_read);
}
while (act_read == ALEN(buf));
send_u8(0);
COM(is, Release);
}
else
send_cerror("no blob data");
}
else
send_string((char*) &row_data[2], len);
}
COM(rowset, ReleaseRows, got_rows, row, NULL, NULL, NULL);
}
send_u8(0);
end:
free(row_data);
if (cd)
{
for (DBORDINAL i = 0; i < n_cols; ++i)
if (cd[i].accessor) COM(accessori, ReleaseAccessor, cd[i].accessor, NULL);
free(cd);
}
if (accessori) COM(accessori, Release);
if (ci) CoTaskMemFree(ci);
if (cii) COM(cii, Release);
if (rowset) COM(rowset, Release);
if (ct) COM(ct, Release);
free(cmd);
}
static wchar_t path[MAX_PATH];
void clean_path(void) { DeleteFileW(path); }
int wmain(int argc, wchar_t** argv, wchar_t** x)
{
if (argc != 2)
{
fprintf(stderr, "Usage: %ls db_name\n", argv[0]);
return EXIT_USER;
}
setmode(fileno(stdin), O_BINARY);
setmode(fileno(stdout), O_BINARY);
CHECK(CoInitializeEx(NULL, COINIT_MULTITHREADED));
// A rant about creating a tmp file. _wtmpnam is buggy in wine, works
// differently if you have microsoft's version of msvcrt installed, so avoid
// it. GetTempFileName creates the file, and sqlce pukes if the target already
// exists. Plus they both depend on creating the target because otherwise
// there's no way to check that the name is really unique. So just give up and
// create a random string.
{
const int RAND_LEN = 32;
if (!GetTempPathW(MAX_PATH - RAND_LEN - 1, path)) WIN_ERR();
wchar_t* p = path + wcslen(path);
if (!SystemFunction036(p, RAND_LEN * sizeof(wchar_t))) WIN_ERR();
// generate a random string consisting 'a'..'z', '0'..'9'
for (int i = 0; i < RAND_LEN; ++i)
p[i] = "a\x16"[(p[i] % 36) >= 26] + (p[i] % 36);
// trailing \0 is there due to static initialization
atexit(clean_path);
}
convert_db(argv[1], path);
IDBInitialize* init = init_oledb();
IDBCreateCommand* creat = open_db(init, path);
#ifdef SINGLE_COMMAND
cmd_exec(creat);
#else
while (true)
{
fflush(stdout);
uint8_t command;
if (fread(&command, sizeof(command), 1, stdin) != 1) break;
switch (command)
{
case 0:
cmd_exec(creat);
break;
default:
break;
}
}
#endif
fflush(stdout);
COM(creat, Release);
COM(init, Release);
return EXIT_OK;
}