1use crate::{DatabaseState, ProfileError, ProfileReader, Result, SchemaReport};
3use rusqlite::{params_from_iter, types::ValueRef, Connection};
4use serde::Serialize;
5use std::collections::{BTreeMap, BTreeSet};
6
7const MAX_PEERS: usize = 200;
8const MAX_CATALOG_ROWS: usize = 4096;
9const MAX_METADATA_TEXT_BYTES: usize = 4096;
10
11#[derive(Clone, Debug, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct PeerMetadata {
14 pub local_type: Option<i64>,
15 pub flag: Option<i64>,
16 pub delete_flag: Option<i64>,
17 pub verify_flag: Option<i64>,
18 pub contact_service_type: Option<i32>,
20 pub business: Option<PeerBusinessMetadata>,
21 pub openim: Option<PeerOpenImMetadata>,
22}
23
24#[derive(Clone, Debug, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct PeerBusinessMetadata {
27 pub service_type: Option<i64>,
28 pub accept_type: Option<i64>,
29 pub child_type: Option<i64>,
30}
31
32#[derive(Clone, Debug, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct PeerOpenImMetadata {
35 pub app_id: Option<String>,
36 pub wording_id: Option<String>,
37 pub wordings: Vec<PeerOpenImWording>,
38 pub applications: Vec<PeerOpenImApplication>,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct PeerOpenImWording {
44 pub language_id: i64,
45 pub wording: String,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct PeerOpenImApplication {
51 pub language_id: i64,
52 pub account_type_id: Option<String>,
53 pub account_type_cached: bool,
54}
55
56fn integer(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<Option<i64>> {
57 Ok(match row.get_ref(index)? {
58 ValueRef::Integer(value) => Some(value),
59 _ => None,
60 })
61}
62
63fn text(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<Option<String>> {
64 Ok(match row.get_ref(index)? {
65 ValueRef::Text(value) if value.len() <= MAX_METADATA_TEXT_BYTES => {
66 std::str::from_utf8(value).ok().map(str::to_owned)
67 }
68 _ => None,
69 })
70}
71
72fn has_table(schema: &SchemaReport, table: &str, columns: &[&str]) -> Result<bool> {
73 let Some(table) = schema.tables.iter().find(|item| item.name == table) else {
74 return Ok(false);
75 };
76 if columns
77 .iter()
78 .all(|name| table.columns.iter().any(|column| column.name == *name))
79 {
80 Ok(true)
81 } else {
82 Err(ProfileError::UnsupportedSchema(schema.database.clone()))
83 }
84}
85
86impl ProfileReader {
87 pub fn peer_metadata(&self, peers: &[String]) -> Result<BTreeMap<String, PeerMetadata>> {
91 if peers.len() > MAX_PEERS
92 || peers
93 .iter()
94 .any(|name| name.is_empty() || name.len() > 4096 || name.contains('\0'))
95 {
96 return Err(ProfileError::InvalidQuery);
97 }
98 if peers.is_empty() {
99 return Ok(BTreeMap::new());
100 }
101 let mut candidates = self
102 .snapshot
103 .inventory
104 .iter()
105 .filter(|item| item.relative_path.rsplit('/').next() == Some("contact.db"));
106 let database = candidates
107 .next()
108 .ok_or_else(|| ProfileError::UnsupportedSchema("contact.db".into()))?;
109 if candidates.next().is_some() {
110 return Err(ProfileError::UnsupportedSchema("contact.db".into()));
111 }
112 match database.state {
113 DatabaseState::KeyUnavailable => {
114 return Err(ProfileError::KeyUnavailable(database.relative_path.clone()))
115 }
116 DatabaseState::KeyMismatch => {
117 return Err(ProfileError::KeyMismatch(database.relative_path.clone()))
118 }
119 _ => {}
120 }
121 let schema = self
122 .schemas
123 .iter()
124 .find(|schema| schema.database == database.relative_path)
125 .ok_or_else(|| ProfileError::UnsupportedSchema(database.relative_path.clone()))?;
126 if !has_table(
127 schema,
128 "contact",
129 &[
130 "id",
131 "username",
132 "local_type",
133 "flag",
134 "delete_flag",
135 "verify_flag",
136 "extra_buffer",
137 ],
138 )? {
139 return Err(ProfileError::UnsupportedSchema(
140 database.relative_path.clone(),
141 ));
142 }
143 let connection = self.snapshot.open(&database.relative_path)?;
144 let transaction = connection
147 .is_autocommit()
148 .then(|| connection.unchecked_transaction())
149 .transpose()?;
150 let result = read_metadata(
151 &connection,
152 schema,
153 peers,
154 self.snapshot.options.max_blob_bytes,
155 );
156 if let Some(transaction) = transaction {
157 transaction.rollback()?;
158 }
159 result
160 }
161}
162
163fn read_metadata(
164 connection: &Connection,
165 schema: &SchemaReport,
166 peers: &[String],
167 max_blob_bytes: usize,
168) -> Result<BTreeMap<String, PeerMetadata>> {
169 let placeholders = vec!["?"; peers.len()].join(",");
170 record_query();
171 let mut statement = connection.prepare(&format!(
172 "SELECT username,local_type,flag,delete_flag,verify_flag,CASE WHEN length(extra_buffer)<={max_blob_bytes} THEN extra_buffer END FROM contact WHERE id IN (SELECT min(id) FROM contact WHERE username IN ({placeholders}) GROUP BY username HAVING count(*)=1) LIMIT {}",
173 MAX_PEERS + 1
174 ))?;
175 let mut rows = statement.query(params_from_iter(peers))?;
176 let mut metadata = BTreeMap::new();
177 let mut count = 0;
178 while let Some(row) = rows.next()? {
179 count += 1;
180 if count > MAX_PEERS {
181 return Err(ProfileError::ResourceLimit);
182 }
183 let Some(name) = text(row, 0)? else {
184 continue;
185 };
186 let local_type = integer(row, 1)?;
187 let bytes = match row.get_ref(5)? {
188 ValueRef::Blob(value) | ValueRef::Text(value) => Some(value),
189 _ => None,
190 };
191 let (contact_service_type, openim) = match local_type {
192 Some(5..=7) => (None, bytes.and_then(|value| decode_openim(value).ok())),
193 Some(0..=4) => (
194 bytes
195 .and_then(|value| decode_service_type(value).ok())
196 .flatten(),
197 None,
198 ),
199 _ => (None, None),
200 };
201 let item = PeerMetadata {
202 local_type,
203 flag: integer(row, 2)?,
204 delete_flag: integer(row, 3)?,
205 verify_flag: integer(row, 4)?,
206 contact_service_type,
207 business: None,
208 openim,
209 };
210 metadata.insert(name, item);
211 }
212 drop(rows);
213 drop(statement);
214 if has_table(
215 schema,
216 "biz_info",
217 &["username", "type", "accept_type", "child_type"],
218 )? {
219 read_business(connection, &placeholders, peers, &mut metadata)?;
220 }
221 read_openim_catalogs(connection, schema, &mut metadata)?;
222 Ok(metadata)
223}
224
225fn read_business(
226 connection: &Connection,
227 placeholders: &str,
228 peers: &[String],
229 metadata: &mut BTreeMap<String, PeerMetadata>,
230) -> Result<()> {
231 record_query();
232 let mut statement = connection.prepare(&format!(
233 "SELECT username,type,accept_type,child_type FROM biz_info WHERE username IN ({placeholders}) GROUP BY username HAVING count(*)=1 LIMIT {}",
234 MAX_PEERS + 1
235 ))?;
236 let mut rows = statement.query(params_from_iter(peers))?;
237 let mut count = 0;
238 while let Some(row) = rows.next()? {
239 count += 1;
240 if count > MAX_PEERS {
241 return Err(ProfileError::ResourceLimit);
242 }
243 let Some(name) = text(row, 0)? else {
244 continue;
245 };
246 let Some(item) = metadata.get_mut(&name) else {
247 continue;
248 };
249 item.business = Some(PeerBusinessMetadata {
250 service_type: integer(row, 1)?,
251 accept_type: integer(row, 2)?,
252 child_type: integer(row, 3)?,
253 });
254 }
255 Ok(())
256}
257
258fn read_openim_catalogs(
259 connection: &Connection,
260 schema: &SchemaReport,
261 metadata: &mut BTreeMap<String, PeerMetadata>,
262) -> Result<()> {
263 let has_wordings = has_table(
264 schema,
265 "openim_wording",
266 &["app_id", "wording_id", "lang_id", "wording"],
267 )?;
268 let has_applications = has_table(
269 schema,
270 "openim_appid",
271 &["app_id", "lang_id", "acct_type_id"],
272 )?;
273 let has_account_types = has_table(schema, "openim_acct_type", &["acc_type_id", "lang_id"])?;
274 let pairs: BTreeSet<_> = metadata
275 .values()
276 .filter_map(|item| {
277 let item = item.openim.as_ref()?;
278 Some((
279 item.app_id.as_ref()?.clone(),
280 item.wording_id.as_ref()?.clone(),
281 ))
282 })
283 .collect();
284 let applications: BTreeSet<_> = metadata
285 .values()
286 .filter_map(|item| item.openim.as_ref()?.app_id.clone())
287 .collect();
288 let mut wordings = BTreeMap::<(String, String), Vec<PeerOpenImWording>>::new();
289 if has_wordings && !pairs.is_empty() {
290 let values = vec!["(?,?)"; pairs.len()].join(",");
291 let arguments: Vec<_> = pairs
292 .iter()
293 .flat_map(|(app, wording)| [app, wording])
294 .collect();
295 record_query();
296 let mut statement = connection.prepare(&format!(
297 "WITH requested(app,wording) AS (VALUES {values}) SELECT w.app_id,w.wording_id,w.lang_id,w.wording FROM requested r CROSS JOIN openim_wording w WHERE w.app_id=r.app AND w.wording_id=r.wording GROUP BY w.app_id,w.wording_id,w.lang_id HAVING count(*)=1 ORDER BY w.app_id,w.wording_id,w.lang_id LIMIT {}",
298 MAX_CATALOG_ROWS + 1
299 ))?;
300 let mut rows = statement.query(params_from_iter(arguments))?;
301 let mut count = 0;
302 while let Some(row) = rows.next()? {
303 count += 1;
304 if count > MAX_CATALOG_ROWS {
305 return Err(ProfileError::ResourceLimit);
306 }
307 if let (Some(app), Some(id), Some(language_id), Some(wording)) = (
308 text(row, 0)?,
309 text(row, 1)?,
310 integer(row, 2)?,
311 text(row, 3)?,
312 ) {
313 wordings
314 .entry((app, id))
315 .or_default()
316 .push(PeerOpenImWording {
317 language_id,
318 wording,
319 });
320 }
321 }
322 }
323 let mut app_records = BTreeMap::<String, Vec<PeerOpenImApplication>>::new();
324 if has_applications && !applications.is_empty() {
325 let values = vec!["(?)"; applications.len()].join(",");
326 let account_join = if has_account_types {
327 "LEFT JOIN openim_acct_type a ON a.acc_type_id=p.acct_type_id AND a.lang_id=p.lang_id"
328 } else {
329 ""
330 };
331 let cached = if has_account_types {
332 "a.acc_type_id IS NOT NULL"
333 } else {
334 "0"
335 };
336 record_query();
337 let mut statement = connection.prepare(&format!(
338 "WITH requested(app) AS (VALUES {values}) SELECT p.app_id,p.lang_id,p.acct_type_id,{cached} FROM requested r CROSS JOIN openim_appid p {account_join} WHERE p.app_id=r.app GROUP BY p.app_id,p.lang_id HAVING count(*)=1 ORDER BY p.app_id,p.lang_id LIMIT {}",
339 MAX_CATALOG_ROWS + 1
340 ))?;
341 let mut rows = statement.query(params_from_iter(&applications))?;
342 let mut count = 0;
343 while let Some(row) = rows.next()? {
344 count += 1;
345 if count > MAX_CATALOG_ROWS {
346 return Err(ProfileError::ResourceLimit);
347 }
348 if let (Some(app), Some(language_id)) = (text(row, 0)?, integer(row, 1)?) {
349 app_records
350 .entry(app)
351 .or_default()
352 .push(PeerOpenImApplication {
353 language_id,
354 account_type_id: text(row, 2)?,
355 account_type_cached: integer(row, 3)? == Some(1),
356 });
357 }
358 }
359 }
360 for item in metadata
361 .values_mut()
362 .filter_map(|item| item.openim.as_mut())
363 {
364 if let (Some(app), Some(id)) = (&item.app_id, &item.wording_id) {
365 item.wordings = wordings
366 .get(&(app.clone(), id.clone()))
367 .cloned()
368 .unwrap_or_default();
369 }
370 if let Some(app) = &item.app_id {
371 item.applications = app_records.get(app).cloned().unwrap_or_default();
372 }
373 }
374 Ok(())
375}
376
377fn decode_service_type(bytes: &[u8]) -> Result<Option<i32>> {
378 let mut service_type = None;
379 walk_fields(bytes, |number, value| {
380 if number == 13 {
381 let WireValue::Varint(value) = value else {
382 return Err(ProfileError::InvalidMetadata);
383 };
384 service_type = Some(value as u32 as i32);
385 }
386 Ok(())
387 })?;
388 Ok(service_type)
389}
390
391fn decode_openim(bytes: &[u8]) -> Result<PeerOpenImMetadata> {
392 let mut item = PeerOpenImMetadata {
393 app_id: None,
394 wording_id: None,
395 wordings: Vec::new(),
396 applications: Vec::new(),
397 };
398 walk_fields(bytes, |number, value| {
399 if matches!(number, 1 | 2) {
400 let WireValue::Bytes(bytes) = value else {
401 return Err(ProfileError::InvalidMetadata);
402 };
403 if bytes.len() > MAX_METADATA_TEXT_BYTES {
404 return Err(ProfileError::ResourceLimit);
405 }
406 let value = std::str::from_utf8(bytes)
407 .map_err(|_| ProfileError::InvalidMetadata)?
408 .to_owned();
409 if number == 1 {
410 item.app_id = Some(value);
411 } else {
412 item.wording_id = Some(value);
413 }
414 }
415 Ok(())
416 })?;
417 Ok(item)
418}
419
420enum WireValue<'a> {
421 Varint(u64),
422 Bytes(&'a [u8]),
423 Other,
424}
425
426fn varint(bytes: &[u8], cursor: &mut usize) -> Result<u64> {
427 let mut value = 0u64;
428 for shift in 0..10 {
429 let byte = *bytes.get(*cursor).ok_or(ProfileError::InvalidMetadata)?;
430 *cursor += 1;
431 if shift == 9 && byte > 1 {
432 return Err(ProfileError::InvalidMetadata);
433 }
434 value |= u64::from(byte & 0x7f) << (shift * 7);
435 if byte & 0x80 == 0 {
436 return Ok(value);
437 }
438 }
439 Err(ProfileError::InvalidMetadata)
440}
441
442fn walk_fields<'a>(
443 bytes: &'a [u8],
444 mut field: impl FnMut(u32, WireValue<'a>) -> Result<()>,
445) -> Result<()> {
446 let mut cursor = 0;
447 let mut groups = Vec::new();
448 let mut count = 0;
449 while cursor < bytes.len() {
450 count += 1;
451 if count > 100_000 {
452 return Err(ProfileError::ResourceLimit);
453 }
454 let tag = varint(bytes, &mut cursor)?;
455 let number = tag >> 3;
456 if number == 0 || number >= 1 << 29 {
457 return Err(ProfileError::InvalidMetadata);
458 }
459 let wire = tag & 7;
460 if wire == 3 {
461 if groups.len() >= 32 {
462 return Err(ProfileError::ResourceLimit);
463 }
464 if groups.is_empty() {
465 field(number as u32, WireValue::Other)?;
466 }
467 groups.push(number);
468 continue;
469 }
470 if wire == 4 {
471 if groups.pop() != Some(number) {
472 return Err(ProfileError::InvalidMetadata);
473 }
474 continue;
475 }
476 let value = match wire {
477 0 => WireValue::Varint(varint(bytes, &mut cursor)?),
478 1 | 2 | 5 => {
479 let length = match wire {
480 1 => 8,
481 5 => 4,
482 _ => usize::try_from(varint(bytes, &mut cursor)?)
483 .map_err(|_| ProfileError::InvalidMetadata)?,
484 };
485 let end = cursor
486 .checked_add(length)
487 .ok_or(ProfileError::InvalidMetadata)?;
488 let value = bytes
489 .get(cursor..end)
490 .ok_or(ProfileError::InvalidMetadata)?;
491 cursor = end;
492 if wire == 2 {
493 WireValue::Bytes(value)
494 } else {
495 WireValue::Other
496 }
497 }
498 _ => return Err(ProfileError::InvalidMetadata),
499 };
500 if groups.is_empty() {
501 field(number as u32, value)?;
502 }
503 }
504 if groups.is_empty() {
505 Ok(())
506 } else {
507 Err(ProfileError::InvalidMetadata)
508 }
509}
510
511fn record_query() {
512 #[cfg(test)]
513 tests::QUERIES.with(|count| count.set(count.get() + 1));
514}
515
516#[cfg(test)]
517mod tests;