using System; using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Reflection; namespace MsgLibReadSidecar { internal static class Program { private const string Version = "0.1.0"; private const string DefaultPassword = "123"; private static readonly HashSet DefaultTargetTables = new HashSet(StringComparer.OrdinalIgnoreCase) { "TD_Roster", "TD_CustomEffigy", "tblRecent", "tblChatLevel", "tblPersonMsg", "tblMsgGroupPersonMsg", "TD_WorkGroupAuth", "TD_ReceiveFileRecord", "TD_SendFileRecord", "TD_SystemMessageRecord", "TD_MsgReadBurn" }; [STAThread] private static int Main(string[] args) { string requestText = Console.In.ReadToEnd(); Dictionary request; string requestId = ""; string op = ""; if (!SidecarProtocol.TryParseRequest(requestText, out request, out requestId, out op)) { Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "INVALID_REQUEST", "request must be a valid isphere.msglib.v1 JSON object"))); return 0; } try { Dictionary opArgs = SidecarProtocol.GetObject(request, "args"); Dictionary response; switch (op) { case "self_check": response = SidecarProtocol.Success(requestId, op, SelfCheckData()); break; case "schema_summary": response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, false)); break; case "display_sources": response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, true)); break; default: response = SidecarProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op); break; } Console.WriteLine(SidecarProtocol.ToJson(response)); return 0; } catch (Exception ex) { Console.Error.WriteLine(ex.ToString()); Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "SIDECAR_FAILED", ex.Message))); return 0; } } private static Dictionary SelfCheckData() { return new Dictionary { { "sidecar_name", "MsgLibReadSidecar" }, { "sidecar_version", Version }, { "protocol", SidecarProtocol.Protocol }, { "runtime", ".NET Framework " + Environment.Version }, { "process_bits", IntPtr.Size * 8 }, { "requires_process_bits", 32 }, { "db_mutation_allowed", false }, { "message_body_reads_allowed", false } }; } private static Dictionary SchemaSummary(Dictionary args, bool displayOnly) { string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", ""); string dbPath = SidecarProtocol.GetString(args, "db_path", ""); string password = SidecarProtocol.GetString(args, "password", DefaultPassword); bool includeCounts = SidecarProtocol.GetBool(args, "include_counts", true); int maxTables = Clamp(SidecarProtocol.GetInt(args, "max_tables", 128), 1, 256); List requestedTargets = SidecarProtocol.GetStringList(args, "target_tables"); HashSet targetTables = requestedTargets.Count == 0 ? DefaultTargetTables : new HashSet(requestedTargets, StringComparer.OrdinalIgnoreCase); ValidateInputs(sqliteDllPath, dbPath); var result = new Dictionary { { "metadata_only", true }, { "read_only", true }, { "message_body_values_returned", false }, { "sqlite_dll_path", sqliteDllPath }, { "db_path", dbPath }, { "provider", "System.Data.SQLite" }, { "process_bits", IntPtr.Size * 8 } }; using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password)) { connection.Open(); TryExecuteNonQuery(connection, "PRAGMA query_only=ON"); result["sqlite_version"] = ExecuteScalar(connection, "select sqlite_version()"); List> tables = ReadMasterTables(connection, maxTables); List> columns = ReadColumns(connection, tables, displayOnly ? targetTables : null); result["tables"] = tables; result["columns"] = columns; result["target_row_counts"] = includeCounts ? ReadTargetCounts(connection, tables, targetTables) : new List>(); if (displayOnly) { result["display_sources"] = BuildDisplaySources(tables, columns); } } return result; } private static void ValidateInputs(string sqliteDllPath, string dbPath) { if (string.IsNullOrWhiteSpace(sqliteDllPath) || !File.Exists(sqliteDllPath)) { throw new ArgumentException("sqlite_dll_path must point to an existing System.Data.SQLite.dll"); } if (string.IsNullOrWhiteSpace(dbPath) || !File.Exists(dbPath)) { throw new ArgumentException("db_path must point to an existing copied MsgLib.db"); } if (!string.Equals(Path.GetFileName(dbPath), "MsgLib.db", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("db_path must point to a copied MsgLib.db file"); } } private static DbConnection OpenSqliteConnection(string sqliteDllPath, string dbPath, string password) { Assembly assembly = Assembly.LoadFrom(sqliteDllPath); Type builderType = assembly.GetType("System.Data.SQLite.SQLiteConnectionStringBuilder", true); object builder = Activator.CreateInstance(builderType); SetPropertyIfExists(builderType, builder, "DataSource", dbPath); SetPropertyIfExists(builderType, builder, "Password", password); SetPropertyIfExists(builderType, builder, "ReadOnly", true); SetPropertyIfExists(builderType, builder, "Pooling", false); SetPropertyIfExists(builderType, builder, "FailIfMissing", true); string connectionString = Convert.ToString(builderType.GetProperty("ConnectionString").GetValue(builder, null)); Type connectionType = assembly.GetType("System.Data.SQLite.SQLiteConnection", true); object connection = Activator.CreateInstance(connectionType, new object[] { connectionString }); DbConnection dbConnection = connection as DbConnection; if (dbConnection == null) { throw new InvalidOperationException("SQLiteConnection is not a DbConnection"); } return dbConnection; } private static void SetPropertyIfExists(Type type, object target, string name, object value) { PropertyInfo property = type.GetProperty(name); if (property != null && property.CanWrite) { property.SetValue(target, value, null); } } private static List> ReadMasterTables(DbConnection connection, int maxTables) { var rows = new List>(); using (DbCommand command = connection.CreateCommand()) { command.CommandText = "select type, name, tbl_name from sqlite_master where type in ('table','view') order by type, name"; using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read() && rows.Count < maxTables) { rows.Add(new Dictionary { { "type", Convert.ToString(reader["type"]) }, { "name", Convert.ToString(reader["name"]) }, { "tbl_name", Convert.ToString(reader["tbl_name"]) } }); } } } return rows; } private static List> ReadColumns(DbConnection connection, List> tables, HashSet onlyTables) { var rows = new List>(); foreach (Dictionary table in tables) { string tableName = Convert.ToString(table["name"]); string tableType = Convert.ToString(table["type"]); if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue; if (onlyTables != null && !onlyTables.Contains(tableName)) continue; using (DbCommand command = connection.CreateCommand()) { command.CommandText = "PRAGMA table_info(" + QuoteIdent(tableName) + ")"; using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read()) { rows.Add(new Dictionary { { "table", tableName }, { "cid", Convert.ToString(reader["cid"]) }, { "name", Convert.ToString(reader["name"]) }, { "type", Convert.ToString(reader["type"]) }, { "notnull", Convert.ToString(reader["notnull"]) }, { "pk", Convert.ToString(reader["pk"]) } }); } } } } return rows; } private static List> ReadTargetCounts(DbConnection connection, List> tables, HashSet targetTables) { var rows = new List>(); foreach (Dictionary table in tables) { string tableName = Convert.ToString(table["name"]); string tableType = Convert.ToString(table["type"]); if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue; if (!targetTables.Contains(tableName)) continue; rows.Add(new Dictionary { { "table", tableName }, { "row_count", ExecuteScalar(connection, "select count(*) from " + QuoteIdent(tableName)) } }); } return rows; } private static List> BuildDisplaySources(List> tables, List> columns) { var tableSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (Dictionary table in tables) { if (string.Equals(Convert.ToString(table["type"]), "table", StringComparison.OrdinalIgnoreCase)) { tableSet.Add(Convert.ToString(table["name"])); } } var columnMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (Dictionary column in columns) { string table = Convert.ToString(column["table"]); string name = Convert.ToString(column["name"]); if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet(StringComparer.OrdinalIgnoreCase); columnMap[table].Add(name); } var sources = new List>(); AddSource(sources, tableSet, columnMap, "contacts_roster", "TD_Roster", new string[] { "JId", "Nick", "GroupName" }); AddSource(sources, tableSet, columnMap, "contacts_effigy", "TD_CustomEffigy", new string[] { "PersonJid", "PersonName" }); AddSource(sources, tableSet, columnMap, "recent_display", "tblRecent", new string[] { "PersonId", "PersonName", "FType" }); AddSource(sources, tableSet, columnMap, "person_message_names", "tblPersonMsg", new string[] { "SndPerson", "SndPersonId", "RecvPerson", "RecvPersonId", "ObjPerson", "ObjPersonId" }); AddSource(sources, tableSet, columnMap, "group_message_names", "tblMsgGroupPersonMsg", new string[] { "MsgGroupId", "MsgGroupName", "SessionPersonId", "SessionPersonName", "GroupType" }); AddSource(sources, tableSet, columnMap, "work_group_auth", "TD_WorkGroupAuth", new string[] { "GroupJID", "GroupName", "ChildGroupJid" }); AddSource(sources, tableSet, columnMap, "received_file_metadata", "TD_ReceiveFileRecord", new string[] { "FileName", "SendPersonJid", "SendPersonName" }); AddSource(sources, tableSet, columnMap, "sent_file_metadata", "TD_SendFileRecord", new string[] { "FileName", "ReceivePersonJid", "ReceivePersonName" }); return sources; } private static void AddSource(List> sources, HashSet tableSet, Dictionary> columnMap, string sourceName, string table, string[] columns) { var presentColumns = new List(); if (columnMap.ContainsKey(table)) { foreach (string column in columns) { if (columnMap[table].Contains(column)) presentColumns.Add(column); } } sources.Add(new Dictionary { { "source", sourceName }, { "table", table }, { "available", tableSet.Contains(table) && presentColumns.Count > 0 }, { "required_columns", columns }, { "present_columns", presentColumns.ToArray() } }); } private static string ExecuteScalar(DbConnection connection, string sql) { using (DbCommand command = connection.CreateCommand()) { command.CommandText = sql; object value = command.ExecuteScalar(); return Convert.ToString(value); } } private static void TryExecuteNonQuery(DbConnection connection, string sql) { try { using (DbCommand command = connection.CreateCommand()) { command.CommandText = sql; command.ExecuteNonQuery(); } } catch { } } private static string QuoteIdent(string value) { return "\"" + value.Replace("\"", "\"\"") + "\""; } private static int Clamp(int value, int min, int max) { if (value < min) return min; if (value > max) return max; return value; } } }