1use crate::{ProfileError, Result};
2use rusqlite::{Connection, OptionalExtension};
3use serde::Serialize;
4use sha2::{Digest, Sha256};
5use std::collections::{BTreeMap, BTreeSet};
6
7#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "camelCase")]
9pub struct ColumnSchema {
10 pub name: String,
11 pub declared_type: String,
12 pub not_null: bool,
13 pub default_sql: Option<String>,
14 pub primary_key_position: i64,
15 pub hidden: i64,
16}
17#[derive(Clone, Debug, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct IndexSchema {
20 pub name: String,
21 pub unique: bool,
22 pub origin: String,
23 pub partial: bool,
24 pub columns: Vec<Option<String>>,
25 pub definition: Option<String>,
26}
27#[derive(Clone, Debug, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct TableSchema {
30 pub name: String,
31 pub definition: Option<String>,
32 pub columns: Vec<ColumnSchema>,
33 pub indexes: Vec<IndexSchema>,
34 pub triggers: Vec<(String, Option<String>)>,
35}
36#[derive(Clone, Debug, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct SchemaReport {
39 pub database: String,
40 pub fingerprint: String,
41 pub user_version: i64,
42 pub application_id: i64,
43 pub sqlite_schema_counter: i64,
45 pub tables: Vec<TableSchema>,
46 pub readable_capabilities: Vec<String>,
47 pub writes_supported: bool,
48}
49pub(crate) fn quote_identifier(s: &str) -> String {
50 format!("\"{}\"", s.replace('"', "\"\""))
51}
52pub(crate) fn message_table(s: &str) -> bool {
53 s.len() == 36 && s.starts_with("Msg_") && s.as_bytes()[4..].iter().all(u8::is_ascii_hexdigit)
54}
55
56pub(crate) fn inspect(c: &Connection, database: &str) -> Result<SchemaReport> {
57 let user_version = c.query_row("PRAGMA user_version", [], |r| r.get(0))?;
58 let application_id = c.query_row("PRAGMA application_id", [], |r| r.get(0))?;
59 let counter = c.query_row("PRAGMA schema_version", [], |r| r.get(0))?;
60 let mut tables = Vec::new();
61 let mut stm =
62 c.prepare("SELECT name,sql FROM sqlite_master WHERE type='table' ORDER BY name")?;
63 let names = stm
64 .query_map([], |r| {
65 Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?))
66 })?
67 .collect::<std::result::Result<Vec<_>, _>>()?;
68 if names.len() > 16384 {
69 return Err(ProfileError::ResourceLimit);
70 }
71 for (name, definition) in names {
72 if definition.as_ref().is_some_and(|s| {
75 s.trim_start()
76 .to_ascii_uppercase()
77 .starts_with("CREATE VIRTUAL TABLE")
78 }) {
79 tables.push(TableSchema {
80 name,
81 definition,
82 columns: Vec::new(),
83 indexes: Vec::new(),
84 triggers: Vec::new(),
85 });
86 continue;
87 }
88 let q = quote_identifier(&name);
89 let mut s = c.prepare(&format!("PRAGMA table_xinfo({q})"))?;
90 let columns = s
91 .query_map([], |r| {
92 Ok(ColumnSchema {
93 name: r.get(1)?,
94 declared_type: r.get(2)?,
95 not_null: r.get::<_, i64>(3)? != 0,
96 default_sql: r.get(4)?,
97 primary_key_position: r.get(5)?,
98 hidden: r.get(6)?,
99 })
100 })?
101 .collect::<std::result::Result<Vec<_>, _>>()?;
102 let mut s = c.prepare(&format!("PRAGMA index_list({q})"))?;
103 let raw = s
104 .query_map([], |r| {
105 Ok((
106 r.get::<_, String>(1)?,
107 r.get::<_, i64>(2)? != 0,
108 r.get::<_, String>(3)?,
109 r.get::<_, i64>(4)? != 0,
110 ))
111 })?
112 .collect::<std::result::Result<Vec<_>, _>>()?;
113 let mut indexes = Vec::new();
114 for (name, unique, origin, partial) in raw {
115 let mut s = c.prepare(&format!("PRAGMA index_info({})", quote_identifier(&name)))?;
116 let columns = s
117 .query_map([], |r| r.get::<_, Option<String>>(2))?
118 .collect::<std::result::Result<Vec<_>, _>>()?;
119 let definition = c
120 .query_row(
121 "SELECT sql FROM sqlite_master WHERE type='index' AND name=?1",
122 [&name],
123 |r| r.get::<_, Option<String>>(0),
124 )
125 .optional()?
126 .flatten();
127 indexes.push(IndexSchema {
128 name,
129 unique,
130 origin,
131 partial,
132 columns,
133 definition,
134 });
135 }
136 indexes.sort_by(|a, b| a.name.cmp(&b.name));
137 let mut s = c.prepare(
138 "SELECT name,sql FROM sqlite_master WHERE type='trigger' AND tbl_name=?1 ORDER BY name",
139 )?;
140 let triggers = s
141 .query_map([&name], |r| Ok((r.get(0)?, r.get(1)?)))?
142 .collect::<std::result::Result<Vec<_>, _>>()?;
143 tables.push(TableSchema {
144 name,
145 definition,
146 columns,
147 indexes,
148 triggers,
149 });
150 }
151 let canonical = serde_json::to_vec(&(user_version, application_id, &tables))
154 .map_err(|_| ProfileError::InvalidMetadata)?;
155 let fingerprint = hex::encode(Sha256::digest(&canonical));
156 let map: BTreeMap<_, BTreeSet<_>> = tables
157 .iter()
158 .map(|t| {
159 (
160 t.name.as_str(),
161 t.columns.iter().map(|v| v.name.as_str()).collect(),
162 )
163 })
164 .collect();
165 let has = |table: &str, columns: &[&str]| {
166 map.get(table)
167 .is_some_and(|s| columns.iter().all(|c| s.contains(c)))
168 };
169 let mut caps = Vec::new();
170 if has(
171 "contact",
172 &[
173 "id",
174 "username",
175 "nick_name",
176 "remark",
177 "alias",
178 "local_type",
179 ],
180 ) {
181 caps.push("contacts".into());
182 }
183 if has("chat_room", &["id", "username", "owner", "ext_buffer"])
184 && has("chatroom_member", &["room_id", "member_id"])
185 {
186 caps.push("groups".into());
187 }
188 if has(
189 "SessionTable",
190 &[
191 "username",
192 "unread_count",
193 "sort_timestamp",
194 "last_timestamp",
195 "is_hidden",
196 ],
197 ) {
198 caps.push("threads".into());
199 }
200 if has(
201 "head_image",
202 &["username", "md5", "image_buffer", "update_time"],
203 ) {
204 caps.push("avatars".into());
205 }
206 if has(
207 "VoiceInfo",
208 &["chat_name_id", "local_id", "svr_id", "voice_data"],
209 ) && has("Name2Id", &["user_name"])
210 {
211 caps.push("voice".into());
212 }
213 if has("Name2Id", &["user_name"])
214 && tables.iter().filter(|t| message_table(&t.name)).any(|t| {
215 [
216 "local_id",
217 "server_id",
218 "local_type",
219 "create_time",
220 "real_sender_id",
221 "message_content",
222 "source",
223 "WCDB_CT_message_content",
224 "WCDB_CT_source",
225 ]
226 .iter()
227 .all(|n| t.columns.iter().any(|c| c.name == *n))
228 })
229 {
230 caps.push("messages".into());
231 }
232 Ok(SchemaReport {
233 database: database.into(),
234 fingerprint,
235 user_version,
236 application_id,
237 sqlite_schema_counter: counter,
238 tables,
239 readable_capabilities: caps,
240 writes_supported: false,
241 })
242}
243
244impl SchemaReport {
245 pub fn supports(&self, capability: &str) -> bool {
246 self.readable_capabilities.iter().any(|v| v == capability)
247 }
248 pub fn require_writable(&self) -> Result<()> {
250 Err(ProfileError::WritesUnsupported)
251 }
252}