1use crate::{
2 model::*,
3 schema::{inspect, message_table, quote_identifier},
4 snapshot::Snapshot,
5 KeyFile, ProfileError, ProfileLocation, ReadOptions, Result, SchemaReport,
6};
7use base64::{
8 engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
9 Engine,
10};
11use md5::Md5;
12use rusqlite::{params, types::ValueRef, Connection, OptionalExtension, Row};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::{
16 collections::{BTreeMap, BTreeSet},
17 io::Read,
18};
19#[path = "thread_previews.rs"]
20mod thread_previews;
21
22pub struct ProfileReader {
23 pub(crate) snapshot: Snapshot,
24 pub(crate) schemas: Vec<SchemaReport>,
25}
26#[derive(Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28struct Cursor {
29 version: u32,
30 generation: String,
31 query: String,
32 key: CursorKey,
33}
34#[derive(Clone, Serialize, Deserialize)]
35#[serde(tag = "type", deny_unknown_fields)]
36enum CursorKey {
37 Name {
38 name: String,
39 },
40 Thread {
41 time: i64,
42 name: String,
43 },
44 Message {
45 time: i64,
46 database: String,
47 table: String,
48 local: i64,
49 },
50}
51
52fn limit(n: Option<usize>) -> Result<usize> {
53 let n = n.unwrap_or(50);
54 if !(1..=200).contains(&n) {
55 Err(ProfileError::InvalidQuery)
56 } else {
57 Ok(n)
58 }
59}
60fn query_hash<T: Serialize>(kind: &str, q: &T) -> Result<String> {
61 let b = serde_json::to_vec(&(kind, q)).map_err(|_| ProfileError::InvalidQuery)?;
62 Ok(hex::encode(Sha256::digest(b)))
63}
64fn encode_cursor(generation: &str, query: &str, key: CursorKey) -> Result<String> {
65 Ok(URL_SAFE_NO_PAD.encode(
66 serde_json::to_vec(&Cursor {
67 version: 1,
68 generation: generation.into(),
69 query: query.into(),
70 key,
71 })
72 .map_err(|_| ProfileError::InvalidCursor)?,
73 ))
74}
75fn decode_cursor(s: Option<&str>, generation: &str, query: &str) -> Result<Option<CursorKey>> {
76 let Some(s) = s else {
77 return Ok(None);
78 };
79 if s.len() > 8192 {
80 return Err(ProfileError::InvalidCursor);
81 }
82 let b = URL_SAFE_NO_PAD
83 .decode(s)
84 .map_err(|_| ProfileError::InvalidCursor)?;
85 let c: Cursor = serde_json::from_slice(&b).map_err(|_| ProfileError::InvalidCursor)?;
86 if c.version != 1 || c.query != query {
87 return Err(ProfileError::InvalidCursor);
88 }
89 if c.generation != generation {
90 return Err(ProfileError::CursorExpired);
91 }
92 Ok(Some(c.key))
93}
94pub(crate) fn raw(row: &Row<'_>) -> rusqlite::Result<RawFields> {
95 let mut out = BTreeMap::new();
96 for (i, name) in row.as_ref().column_names().iter().enumerate() {
97 let value = match row.get_ref(i)? {
98 ValueRef::Null => SqlValue::Null,
99 ValueRef::Integer(n) => SqlValue::Integer(n.to_string()),
100 ValueRef::Real(n) => SqlValue::Real(n.to_string()),
101 ValueRef::Blob(b) => SqlValue::Blob(STANDARD.encode(b)),
102 ValueRef::Text(b) => match std::str::from_utf8(b) {
103 Ok(s) => SqlValue::Text(s.into()),
104 Err(_) => SqlValue::TextBytes(STANDARD.encode(b)),
105 },
106 };
107 out.insert((*name).into(), value);
108 }
109 Ok(out)
110}
111fn text(r: &RawFields, k: &str) -> Option<String> {
112 match r.get(k) {
113 Some(SqlValue::Text(s)) => Some(s.clone()),
114 _ => None,
115 }
116}
117pub(crate) fn num(r: &RawFields, k: &str) -> Option<i64> {
118 match r.get(k) {
119 Some(SqlValue::Integer(s)) => s.parse().ok(),
120 _ => None,
121 }
122}
123fn blob(r: &RawFields, k: &str) -> Result<Option<Vec<u8>>> {
124 match r.get(k) {
125 Some(SqlValue::Text(s)) => Ok(Some(s.as_bytes().to_vec())),
126 Some(SqlValue::Blob(s) | SqlValue::TextBytes(s)) => Ok(Some(
127 STANDARD
128 .decode(s)
129 .map_err(|_| ProfileError::InvalidMetadata)?,
130 )),
131 _ => Ok(None),
132 }
133}
134fn label(r: &RawFields, username: &str) -> String {
135 text(r, "remark")
136 .filter(|s| !s.is_empty())
137 .or_else(|| text(r, "nick_name").filter(|s| !s.is_empty()))
138 .unwrap_or_else(|| username.into())
139}
140fn contact(r: RawFields) -> Result<Contact> {
141 let username = text(&r, "username").ok_or(ProfileError::InvalidMetadata)?;
142 let id = num(&r, "id").ok_or(ProfileError::InvalidMetadata)?;
143 Ok(Contact {
144 label: label(&r, &username),
145 is_group: username.ends_with("@chatroom"),
146 username,
147 local_id: id.to_string(),
148 nickname: text(&r, "nick_name"),
149 remark: text(&r, "remark"),
150 alias: text(&r, "alias"),
151 local_type: num(&r, "local_type"),
152 raw_fields: r,
153 })
154}
155fn group_member(contact: Contact, display: Option<String>) -> GroupMember {
156 let label = contact
157 .remark
158 .clone()
159 .filter(|s| !s.is_empty())
160 .or_else(|| display.clone().filter(|s| !s.is_empty()))
161 .or_else(|| contact.nickname.clone().filter(|s| !s.is_empty()))
162 .unwrap_or_else(|| contact.username.clone());
163 GroupMember {
164 username: contact.username.clone(),
165 local_id: contact.local_id.clone(),
166 label,
167 room_display_name: display,
168 contact,
169 }
170}
171pub(crate) fn decode(r: &RawFields, column: &str, flag: &str, max: usize) -> Result<DecodedField> {
172 let flag = num(r, flag);
173 let Some(mut bytes) = blob(r, column)? else {
174 return Ok(DecodedField {
175 text: None,
176 state: DecodeState::Null,
177 compression_flag: flag,
178 });
179 };
180 if bytes.len() > max {
181 return Err(ProfileError::ResourceLimit);
182 }
183 let mut state = DecodeState::Utf8;
184 if flag.is_some_and(|f| f != 0 && f != 4) {
185 return Ok(DecodedField {
186 text: None,
187 state: DecodeState::UnsupportedCompression,
188 compression_flag: flag,
189 });
190 }
191 if bytes.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) {
192 let result = (|| {
193 let mut decoder = zstd::stream::read::Decoder::new(bytes.as_slice())
194 .ok()?
195 .take((max + 1) as u64);
196 let mut output = Vec::new();
197 decoder.read_to_end(&mut output).ok()?;
198 if output.len() > max {
199 return None;
200 }
201 Some(output)
202 })();
203 let Some(decoded) = result else {
204 return Ok(DecodedField {
205 text: None,
206 state: DecodeState::InvalidCompressedData,
207 compression_flag: flag,
208 });
209 };
210 bytes = decoded;
211 state = DecodeState::Zstd;
212 }
213 match String::from_utf8(bytes) {
214 Ok(s) => Ok(DecodedField {
215 text: Some(s),
216 state,
217 compression_flag: flag,
218 }),
219 Err(_) => Ok(DecodedField {
220 text: None,
221 state: DecodeState::InvalidUtf8,
222 compression_flag: flag,
223 }),
224 }
225}
226fn name_map(c: &Connection) -> Result<BTreeMap<i64, String>> {
227 let present: bool = c.query_row(
228 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='Name2Id')",
229 [],
230 |r| r.get(0),
231 )?;
232 if !present {
233 let messages: bool = c.query_row(
236 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name GLOB 'Msg_*')",
237 [],
238 |r| r.get(0),
239 )?;
240 if messages {
241 return Err(ProfileError::UnsupportedSchema(
242 "message sender namespace".into(),
243 ));
244 }
245 return Ok(BTreeMap::new());
246 }
247 let mut s = c.prepare("SELECT rowid,user_name FROM Name2Id")?;
248 let map = s
249 .query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?
250 .collect::<std::result::Result<BTreeMap<_, _>, _>>()?;
251 if map.len() > 1_000_000 {
252 return Err(ProfileError::ResourceLimit);
253 }
254 Ok(map)
255}
256fn shard(name: &str) -> bool {
257 let b = name.rsplit('/').next().unwrap_or("");
258 let stem = b.strip_suffix(".db").unwrap_or("");
259 ["message_", "biz_message_"].iter().any(|p| {
260 stem.strip_prefix(p)
261 .is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
262 })
263}
264fn validate_name(s: &str) -> Result<()> {
265 if s.is_empty() || s.len() > 4096 || s.contains('\0') {
266 Err(ProfileError::InvalidQuery)
267 } else {
268 Ok(())
269 }
270}
271
272impl ProfileReader {
273 pub fn open(location: ProfileLocation, keys: KeyFile, options: ReadOptions) -> Result<Self> {
274 let snapshot = Snapshot::create(location, keys, options)?;
275 let mut schemas = Vec::new();
276 for db in &snapshot.inventory {
277 if db.state == DatabaseState::Readable {
278 let connection = snapshot.open(&db.relative_path)?;
279 schemas.push(
280 inspect(&connection, &db.relative_path)
281 .map_err(|_| ProfileError::UnsupportedSchema(db.relative_path.clone()))?,
282 );
283 }
284 }
285 Ok(Self { snapshot, schemas })
286 }
287 pub fn generation(&self) -> &str {
288 &self.snapshot.generation
289 }
290 pub fn schemas(&self) -> &[SchemaReport] {
291 &self.schemas
292 }
293 pub fn coverage(&self) -> Coverage {
294 Coverage {
295 partial: self
296 .snapshot
297 .inventory
298 .iter()
299 .any(|d| d.state != DatabaseState::Readable),
300 databases: self.snapshot.inventory.clone(),
301 }
302 }
303 pub fn inventory(&self) -> &[DatabaseInventory] {
304 &self.snapshot.inventory
305 }
306 pub fn require_writable(&self) -> Result<()> {
307 Err(ProfileError::WritesUnsupported)
308 }
309 fn role(&self, basename: &str, capability: &str) -> Result<&str> {
310 let candidates: Vec<_> = self
311 .snapshot
312 .inventory
313 .iter()
314 .filter(|v| v.relative_path.rsplit('/').next() == Some(basename))
315 .collect();
316 if candidates.len() != 1 {
317 return Err(ProfileError::UnsupportedSchema(basename.into()));
318 }
319 let db = candidates[0];
320 match db.state {
321 DatabaseState::KeyUnavailable => {
322 return Err(ProfileError::KeyUnavailable(db.relative_path.clone()))
323 }
324 DatabaseState::KeyMismatch => {
325 return Err(ProfileError::KeyMismatch(db.relative_path.clone()))
326 }
327 _ => {}
328 }
329 if !self
330 .schemas
331 .iter()
332 .any(|s| s.database == db.relative_path && s.supports(capability))
333 {
334 return Err(ProfileError::UnsupportedSchema(db.relative_path.clone()));
335 }
336 Ok(&db.relative_path)
337 }
338 fn page<T>(
339 &self,
340 mut items: Vec<T>,
341 n: usize,
342 q: &str,
343 key: impl Fn(&T) -> CursorKey,
344 ) -> Result<Page<T>> {
345 let more = items.len() > n;
346 items.truncate(n);
347 let next_cursor = if more {
348 items
349 .last()
350 .map(|v| encode_cursor(self.generation(), q, key(v)))
351 .transpose()?
352 } else {
353 None
354 };
355 Ok(Page {
356 items,
357 next_cursor,
358 source_generation: self.generation().into(),
359 coverage: self.coverage(),
360 })
361 }
362 pub fn contacts(&self, q: &ListQuery) -> Result<Page<Contact>> {
363 let n = limit(q.limit)?;
364 let hash = query_hash("contacts", &q.search)?;
365 let after = match decode_cursor(q.cursor.as_deref(), self.generation(), &hash)? {
366 None => String::new(),
367 Some(CursorKey::Name { name }) => name,
368 _ => return Err(ProfileError::InvalidCursor),
369 };
370 let c = self.snapshot.open(self.role("contact.db", "contacts")?)?;
371 let mut s=c.prepare("SELECT * FROM contact WHERE username>?1 AND (?2 IS NULL OR instr(username,?2)>0 OR instr(coalesce(nick_name,''),?2)>0 OR instr(coalesce(remark,''),?2)>0 OR instr(coalesce(alias,''),?2)>0) ORDER BY username LIMIT ?3")?;
372 let records = s
373 .query_map(params![after, q.search, n + 1], raw)?
374 .collect::<std::result::Result<Vec<_>, _>>()?;
375 let items = records
376 .into_iter()
377 .map(contact)
378 .collect::<Result<Vec<_>>>()?;
379 self.page(items, n, &hash, |v| CursorKey::Name {
380 name: v.username.clone(),
381 })
382 }
383 pub fn thread_exists(&self, username: &str) -> Result<bool> {
384 validate_name(username)?;
385 let c = self.snapshot.open(self.role("session.db", "threads")?)?;
386 Ok(c.query_row(
387 "SELECT EXISTS(SELECT 1 FROM SessionTable WHERE username=?1)",
388 [username],
389 |row| row.get(0),
390 )?)
391 }
392 pub fn threads(&self, q: &ListQuery) -> Result<Page<Thread>> {
393 let n = limit(q.limit)?;
394 let hash = query_hash("threads", &q.search)?;
395 let after = match decode_cursor(q.cursor.as_deref(), self.generation(), &hash)? {
396 None => None,
397 Some(CursorKey::Thread { time, name }) => Some((time, name)),
398 _ => return Err(ProfileError::InvalidCursor),
399 };
400 let c = self.snapshot.open(self.role("session.db", "threads")?)?;
401 let mut s=c.prepare("SELECT * FROM SessionTable WHERE (?1 IS NULL OR coalesce(sort_timestamp,0)<?1 OR (coalesce(sort_timestamp,0)=?1 AND username>?2)) AND (?3 IS NULL OR instr(username,?3)>0) ORDER BY coalesce(sort_timestamp,0) DESC,username ASC LIMIT ?4")?;
402 let records = s
403 .query_map(
404 params![
405 after.as_ref().map(|a| a.0),
406 after.as_ref().map(|a| &a.1),
407 q.search,
408 n + 1
409 ],
410 raw,
411 )?
412 .collect::<std::result::Result<Vec<_>, _>>()?;
413 let labels = self.labels()?;
414 let items = records
415 .into_iter()
416 .map(|r| {
417 let username = text(&r, "username").ok_or(ProfileError::InvalidMetadata)?;
418 Ok(Thread {
419 label: labels
420 .get(&username)
421 .cloned()
422 .unwrap_or_else(|| username.clone()),
423 username,
424 unread_count: num(&r, "unread_count"),
425 hidden: num(&r, "is_hidden").map(|n| n != 0),
426 sort_timestamp: num(&r, "sort_timestamp"),
427 last_timestamp: num(&r, "last_timestamp"),
428 raw_fields: r,
429 })
430 })
431 .collect::<Result<Vec<_>>>()?;
432 self.page(items, n, &hash, |v| CursorKey::Thread {
433 time: v.sort_timestamp.unwrap_or(0),
434 name: v.username.clone(),
435 })
436 }
437 pub fn labels(&self) -> Result<BTreeMap<String, String>> {
438 let Ok(rel) = self.role("contact.db", "contacts") else {
440 return Ok(BTreeMap::new());
441 };
442 let c = self.snapshot.open(rel)?;
443 let mut s = c.prepare("SELECT username,remark,nick_name FROM contact LIMIT 1000001")?;
444 let mut out = BTreeMap::new();
445 let mut rows = s.query([])?;
446 while let Some(r) = rows.next()? {
447 let name: String = r.get(0)?;
448 let remark: Option<String> = r.get(1)?;
449 let nick: Option<String> = r.get(2)?;
450 out.insert(
451 name.clone(),
452 remark
453 .filter(|s| !s.is_empty())
454 .or_else(|| nick.filter(|s| !s.is_empty()))
455 .unwrap_or(name),
456 );
457 if out.len() > 1_000_000 {
458 return Err(ProfileError::ResourceLimit);
459 }
460 }
461 Ok(out)
462 }
463 pub fn message_references(
468 &self,
469 chat: Option<&str>,
470 max_items: usize,
471 ) -> Result<Vec<MessageReference>> {
472 if max_items == 0 || max_items > 100_000 {
473 return Err(ProfileError::InvalidQuery);
474 }
475 if let Some(chat) = chat {
476 validate_name(chat)?;
477 }
478 let target = chat.map(|s| format!("Msg_{:x}", Md5::digest(s.as_bytes())));
479 let mut names: BTreeSet<String> = self.labels()?.into_keys().collect();
480 if let Some(chat) = chat {
481 names.insert(chat.to_owned());
482 }
483 let mut records = Vec::new();
484 let mut found = false;
485 for db in self
486 .snapshot
487 .inventory
488 .iter()
489 .filter(|v| shard(&v.relative_path))
490 {
491 if db.state != DatabaseState::Readable {
492 continue;
493 }
494 let schema = self
495 .schemas
496 .iter()
497 .find(|s| s.database == db.relative_path)
498 .ok_or(ProfileError::InvalidMetadata)?;
499 let c = self.snapshot.open(&db.relative_path)?;
500 names.extend(name_map(&c)?.into_values());
501 for table in schema.tables.iter().filter(|t| {
502 message_table(&t.name)
503 && target
504 .as_ref()
505 .is_none_or(|wanted| wanted.eq_ignore_ascii_case(&t.name))
506 }) {
507 require_message_columns(table, &db.relative_path)?;
508 found = true;
509 let sql = format!(
510 "SELECT local_id,create_time FROM {} LIMIT ?1",
511 quote_identifier(&table.name)
512 );
513 let mut statement = c.prepare(&sql)?;
514 let mut rows = statement.query([max_items + 1 - records.len()])?;
515 while let Some(row) = rows.next()? {
516 let local: i64 = row.get(0)?;
517 let time: i64 = row.get(1)?;
518 records.push((time, db.relative_path.clone(), table.name.clone(), local));
519 if records.len() > max_items {
520 return Err(ProfileError::ResourceLimit);
521 }
522 }
523 }
524 }
525 if !found
526 && self
527 .snapshot
528 .inventory
529 .iter()
530 .any(|d| shard(&d.relative_path) && d.state != DatabaseState::Readable)
531 {
532 return Err(ProfileError::KeyUnavailable("message shards".into()));
533 }
534 let mut hashes: BTreeMap<String, Vec<String>> = BTreeMap::new();
535 for name in names {
536 hashes
537 .entry(format!("{:x}", Md5::digest(name.as_bytes())))
538 .or_default()
539 .push(name);
540 }
541 records.sort_unstable_by(|a, b| b.cmp(a));
542 Ok(records
543 .into_iter()
544 .map(|(_, database, table, local)| {
545 let peer = hashes
546 .get(&table[4..].to_ascii_lowercase())
547 .filter(|v| v.len() == 1)
548 .map(|v| v[0].clone());
549 MessageReference {
550 id: SourceMessageId {
551 database,
552 table,
553 local_id: local.to_string(),
554 },
555 peer,
556 }
557 })
558 .collect())
559 }
560
561 pub fn latest_messages(
565 &self,
566 chats: &[String],
567 own_username: Option<&str>,
568 ) -> Result<BTreeMap<String, Message>> {
569 if chats.len() > 200 {
570 return Err(ProfileError::InvalidQuery);
571 }
572 for chat in chats {
573 validate_name(chat)?;
574 }
575 if let Some(own) = own_username {
576 validate_name(own)?;
577 }
578 if chats.is_empty() {
579 return Ok(BTreeMap::new());
580 }
581 let shards: Vec<_> = self
582 .inventory()
583 .iter()
584 .filter(|db| shard(&db.relative_path))
585 .collect();
586 if shards.is_empty() || shards.iter().any(|db| db.state != DatabaseState::Readable) {
587 return Err(ProfileError::KeyUnavailable("message shards".into()));
588 }
589 let labels = BTreeMap::new();
592 let mut result: BTreeMap<String, Message> = BTreeMap::new();
593 for db in shards {
594 let schema = self
595 .schemas
596 .iter()
597 .find(|s| s.database == db.relative_path)
598 .ok_or(ProfileError::InvalidMetadata)?;
599 let c = self.snapshot.open(&db.relative_path)?;
600 let names = name_map(&c)?;
601 let mut selected = Vec::new();
602 let mut columns = BTreeSet::new();
603 for chat in chats {
604 let table = format!("Msg_{:x}", Md5::digest(chat.as_bytes()));
605 let Some(table) = schema
606 .tables
607 .iter()
608 .find(|t| t.name.eq_ignore_ascii_case(&table))
609 else {
610 continue;
611 };
612 if ![
613 "local_id",
614 "server_id",
615 "local_type",
616 "create_time",
617 "real_sender_id",
618 "message_content",
619 "source",
620 "WCDB_CT_message_content",
621 "WCDB_CT_source",
622 ]
623 .iter()
624 .all(|name| table.columns.iter().any(|c| c.name == *name))
625 {
626 return Err(ProfileError::UnsupportedSchema(db.relative_path.clone()));
627 }
628 columns.extend(table.columns.iter().map(|column| column.name.clone()));
629 selected.push((chat, table));
630 }
631 if selected.is_empty() {
632 continue;
633 }
634 let mut discriminator = "__agent_wechat_preview".to_string();
637 while columns.contains(&discriminator) {
638 discriminator.push('_');
639 }
640 let sql = selected.iter().enumerate().map(|(index, (_, table))| {
641 let projection = columns.iter().map(|column| {
642 if table.columns.iter().any(|c| c.name == *column) {
643 quote_identifier(column)
644 } else {
645 format!("NULL AS {}", quote_identifier(column))
646 }
647 }).collect::<Vec<_>>().join(",");
648 format!("SELECT {index} AS {},{projection} FROM (SELECT * FROM {} ORDER BY create_time DESC,local_id DESC LIMIT 1)", quote_identifier(&discriminator), quote_identifier(&table.name))
649 }).collect::<Vec<_>>().join(" UNION ALL ");
650 #[cfg(test)]
651 batch_lookup_tests::record_batch_query();
652 let mut statement = c.prepare(&sql)?;
653 let mut rows = statement.query([])?;
654 while let Some(row) = rows.next()? {
655 let index: usize = row.get(0)?;
656 let (chat, table) = selected.get(index).ok_or(ProfileError::InvalidMetadata)?;
657 let mut fields = raw(row)?;
658 fields.retain(|name, _| table.columns.iter().any(|column| column.name == *name));
659 let local = num(&fields, "local_id")
660 .filter(|id| *id > 0)
661 .ok_or(ProfileError::InvalidMetadata)?;
662 let message = self.message_from_fields_with_peer(
663 fields,
664 &SourceMessageId {
665 database: db.relative_path.clone(),
666 table: table.name.clone(),
667 local_id: local.to_string(),
668 },
669 own_username,
670 &labels,
671 &names,
672 Some(chat),
673 )?;
674 if result
675 .get(*chat)
676 .is_none_or(|old| message_order(&message) > message_order(old))
677 {
678 result.insert((*chat).clone(), message);
679 }
680 }
681 }
682 Ok(result)
683 }
684
685 pub fn messages(&self, q: &MessageQuery) -> Result<Page<Message>> {
686 let n = limit(q.limit)?;
687 if let Some(chat) = &q.chat {
688 validate_name(chat)?;
689 }
690 if let Some(own) = &q.own_username {
691 validate_name(own)?;
692 }
693 let hash = query_hash("messages", &(&q.chat, &q.own_username, q.message_type))?;
694 let after = match decode_cursor(q.cursor.as_deref(), self.generation(), &hash)? {
695 None => None,
696 Some(CursorKey::Message {
697 time,
698 database,
699 table,
700 local,
701 }) if message_table(&table) => Some((time, database, table, local)),
702 _ => return Err(ProfileError::InvalidCursor),
703 };
704 let target = q
705 .chat
706 .as_ref()
707 .map(|s| format!("Msg_{:x}", Md5::digest(s.as_bytes())));
708 let mut all_names: BTreeSet<String> = self.labels()?.into_keys().collect();
709 for db in self
710 .snapshot
711 .inventory
712 .iter()
713 .filter(|v| shard(&v.relative_path) && v.state == DatabaseState::Readable)
714 {
715 let c = self.snapshot.open(&db.relative_path)?;
716 all_names.extend(name_map(&c)?.into_values());
717 }
718 if let Some(chat) = &q.chat {
719 all_names.insert(chat.clone());
720 }
721 let mut hashes: BTreeMap<String, Vec<String>> = BTreeMap::new();
722 for name in all_names {
723 hashes
724 .entry(format!("{:x}", Md5::digest(name.as_bytes())))
725 .or_default()
726 .push(name);
727 }
728 let mut items = Vec::new();
729 let mut found = false;
730 for db in self
731 .snapshot
732 .inventory
733 .iter()
734 .filter(|v| shard(&v.relative_path))
735 {
736 if db.state != DatabaseState::Readable {
737 continue;
738 }
739 let schema = self
740 .schemas
741 .iter()
742 .find(|s| s.database == db.relative_path)
743 .ok_or(ProfileError::InvalidMetadata)?;
744 let c = self.snapshot.open(&db.relative_path)?;
745 let names = name_map(&c)?;
746 for table in schema.tables.iter().filter(|t| {
747 message_table(&t.name)
748 && target
749 .as_ref()
750 .is_none_or(|wanted| wanted.eq_ignore_ascii_case(&t.name))
751 }) {
752 let table_name = &table.name;
753 require_message_columns(table, &db.relative_path)?;
754 found = true;
755 let (max_time, equal_mode, local_bound) =
756 if let Some((time, ref database, ref last_table, local)) = after {
757 let cmp = (&db.relative_path, table_name).cmp(&(database, last_table));
758 (
759 Some(time),
760 if cmp.is_lt() {
761 1
762 } else if cmp.is_gt() {
763 -1
764 } else {
765 0
766 },
767 Some(local),
768 )
769 } else {
770 (None, 0, None)
771 };
772 let sql=format!("SELECT * FROM {} WHERE (?1 IS NULL OR create_time<?1 OR (create_time=?1 AND (?2=1 OR (?2=0 AND local_id<?3)))) AND (?4 IS NULL OR (local_type & 2147483647)=?4) ORDER BY create_time DESC,local_id DESC LIMIT ?5",quote_identifier(table_name));
773 let mut s = c.prepare(&sql)?;
774 let records = s
775 .query_map(
776 params![max_time, equal_mode, local_bound, q.message_type, n + 1],
777 raw,
778 )?
779 .collect::<std::result::Result<Vec<_>, _>>()?;
780 let peer = hashes
781 .get(&table_name[4..].to_ascii_lowercase())
782 .filter(|v| v.len() == 1)
783 .map(|v| v[0].clone());
784 for r in records {
785 let local = num(&r, "local_id").ok_or(ProfileError::InvalidMetadata)?;
786 let signed = num(&r, "server_id");
787 let sender = num(&r, "real_sender_id").and_then(|id| names.get(&id).cloned());
788 let outgoing = q
789 .own_username
790 .as_ref()
791 .and_then(|own| sender.as_ref().map(|s| s == own));
792 let local_type = num(&r, "local_type").ok_or(ProfileError::InvalidMetadata)?;
793 items.push(Message {
794 id: SourceMessageId {
795 database: db.relative_path.clone(),
796 table: table_name.clone(),
797 local_id: local.to_string(),
798 },
799 server_message_id: signed
800 .filter(|v| *v != 0)
801 .map(|v| (v as u64).to_string()),
802 server_id_signed: signed.map(|n| n.to_string()),
803 peer: peer.clone(),
804 sender,
805 outgoing,
806 message_type: local_type & 0x7fff_ffff,
807 local_type: local_type.to_string(),
808 create_time: num(&r, "create_time").ok_or(ProfileError::InvalidMetadata)?,
809 sort_seq: num(&r, "sort_seq").map(|n| n.to_string()),
810 content: decode(
811 &r,
812 "message_content",
813 "WCDB_CT_message_content",
814 self.snapshot.options.max_blob_bytes,
815 )?,
816 source: decode(
817 &r,
818 "source",
819 "WCDB_CT_source",
820 self.snapshot.options.max_blob_bytes,
821 )?,
822 raw_fields: r,
823 });
824 }
825 items.sort_by(|a, b| message_order(b).cmp(&message_order(a)));
826 items.truncate(n + 1);
827 }
828 }
829 if !found
830 && self
831 .snapshot
832 .inventory
833 .iter()
834 .any(|d| shard(&d.relative_path) && d.state != DatabaseState::Readable)
835 {
836 return Err(ProfileError::KeyUnavailable("message shards".into()));
837 }
838 self.page(items, n, &hash, |v| CursorKey::Message {
839 time: v.create_time,
840 database: v.id.database.clone(),
841 table: v.id.table.clone(),
842 local: v.id.local_id.parse().unwrap_or(0),
843 })
844 }
845
846 pub fn message(&self, id: &SourceMessageId, own_username: Option<&str>) -> Result<Message> {
849 if !message_table(&id.table) || !shard(&id.database) {
850 return Err(ProfileError::InvalidQuery);
851 }
852 let c = self.snapshot.open(&id.database)?;
853 let names = name_map(&c)?;
854 let labels = self.labels()?;
855 self.message_from_connection(&c, id, own_username, &labels, &names)
856 }
857
858 pub fn messages_by_ids(
862 &self,
863 ids: &[SourceMessageId],
864 own_username: Option<&str>,
865 ) -> Result<Vec<Message>> {
866 if ids.len() > 200 {
867 return Err(ProfileError::InvalidQuery);
868 }
869 if ids.is_empty() {
870 return Ok(Vec::new());
871 }
872 if let Some(own) = own_username {
873 validate_name(own)?;
874 }
875 let mut groups: BTreeMap<&str, Vec<&SourceMessageId>> = BTreeMap::new();
876 for id in ids {
877 if !message_table(&id.table) || !shard(&id.database) {
878 return Err(ProfileError::InvalidQuery);
879 }
880 groups.entry(&id.database).or_default().push(id);
881 }
882 let labels = self.labels()?;
883 let mut result = Vec::with_capacity(ids.len());
884 for (database, group) in groups {
885 let c = self.snapshot.open(database)?;
886 let names = name_map(&c)?;
887 for id in group {
888 match self.message_from_connection(&c, id, own_username, &labels, &names) {
889 Ok(message) => result.push(message),
890 Err(ProfileError::NotCached) => {}
891 Err(error) => return Err(error),
892 }
893 }
894 }
895 Ok(result)
896 }
897
898 pub fn messages_by_server_id(
901 &self,
902 chat: &str,
903 server_id: &str,
904 own_username: Option<&str>,
905 ) -> Result<Vec<Message>> {
906 validate_name(chat)?;
907 let server = server_id
908 .parse::<u64>()
909 .map_err(|_| ProfileError::InvalidQuery)?;
910 if server == 0 || server.to_string() != server_id {
911 return Err(ProfileError::InvalidQuery);
912 }
913 let table_name = format!("Msg_{:x}", Md5::digest(chat.as_bytes()));
914 let labels = self.labels()?;
915 let mut result = Vec::new();
916 let shards: Vec<_> = self.schemas.iter().filter(|s| shard(&s.database)).collect();
917 if shards.len() > 128 {
918 return Err(ProfileError::InvalidQuery);
919 }
920 if self
921 .coverage()
922 .databases
923 .iter()
924 .any(|d| shard(&d.relative_path) && d.state != DatabaseState::Readable)
925 {
926 return Err(ProfileError::KeyUnavailable("message shards".into()));
927 }
928 for schema in shards {
929 let Some(table) = schema.tables.iter().find(|t| t.name == table_name) else {
930 continue;
931 };
932 if !["local_id", "server_id"]
933 .iter()
934 .all(|name| table.columns.iter().any(|c| c.name == *name))
935 {
936 return Err(ProfileError::UnsupportedSchema(schema.database.clone()));
937 }
938 let c = self.snapshot.open(&schema.database)?;
939 let names = name_map(&c)?;
940 let mut statement = c.prepare(&format!(
941 "SELECT local_id FROM {} WHERE server_id=?1 LIMIT 2",
942 quote_identifier(&table_name)
943 ))?;
944 let ids = statement
945 .query_map([server as i64], |row| row.get::<_, i64>(0))?
946 .collect::<std::result::Result<Vec<_>, _>>()?;
947 for local in ids {
948 let mut message = self.message_from_connection(
949 &c,
950 &SourceMessageId {
951 database: schema.database.clone(),
952 table: table_name.clone(),
953 local_id: local.to_string(),
954 },
955 own_username,
956 &labels,
957 &names,
958 )?;
959 message.peer = Some(chat.into());
962 result.push(message);
963 if result.len() == 2 {
964 return Ok(result);
965 }
966 }
967 }
968 Ok(result)
969 }
970
971 pub fn visit_messages_by_server_ids<E>(
976 &self,
977 targets: &[(String, String)],
978 own_username: Option<&str>,
979 mut visit: impl FnMut(Message) -> std::result::Result<(), E>,
980 ) -> std::result::Result<(), E>
981 where
982 E: From<ProfileError> + From<rusqlite::Error>,
983 {
984 if targets.len() > 200 {
985 return Err(ProfileError::InvalidQuery.into());
986 }
987 if targets.is_empty() {
988 return Ok(());
989 }
990 if let Some(own) = own_username {
991 validate_name(own)?;
992 }
993 let mut groups: BTreeMap<&str, BTreeSet<i64>> = BTreeMap::new();
994 for (chat, server) in targets {
995 validate_name(chat)?;
996 let number = server
997 .parse::<u64>()
998 .map_err(|_| ProfileError::InvalidQuery)?;
999 if number == 0 || number.to_string() != *server {
1000 return Err(ProfileError::InvalidQuery.into());
1001 }
1002 groups.entry(chat).or_default().insert(number as i64);
1003 }
1004 let shards: Vec<_> = self.schemas.iter().filter(|s| shard(&s.database)).collect();
1005 if shards.len() > 128 {
1006 return Err(ProfileError::InvalidQuery.into());
1007 }
1008 if self
1009 .coverage()
1010 .databases
1011 .iter()
1012 .any(|d| shard(&d.relative_path) && d.state != DatabaseState::Readable)
1013 {
1014 return Err(ProfileError::KeyUnavailable("message shards".into()).into());
1015 }
1016 let labels = self.labels()?;
1017 let mut counts: BTreeMap<(&str, i64), usize> = BTreeMap::new();
1018 for schema in shards {
1019 let tables: Vec<_> = groups
1020 .iter()
1021 .filter_map(|(&chat, servers)| {
1022 let table_name = format!("Msg_{:x}", Md5::digest(chat.as_bytes()));
1023 let table = schema.tables.iter().find(|t| t.name == table_name)?;
1024 let pending: Vec<_> = servers
1025 .iter()
1026 .copied()
1027 .filter(|server| counts.get(&(chat, *server)).copied().unwrap_or(0) < 2)
1028 .collect();
1029 (!pending.is_empty()).then_some((chat, table, pending))
1030 })
1031 .collect();
1032 if tables.is_empty() {
1033 continue;
1034 }
1035 let c = self.snapshot.open(&schema.database)?;
1036 let names = name_map(&c)?;
1037 for (chat, table, servers) in tables {
1038 require_message_columns(table, &schema.database)?;
1039 let table_name = quote_identifier(&table.name);
1040 let placeholders = vec!["?"; servers.len()].join(",");
1041 let sql = format!("SELECT * FROM {table_name} WHERE local_id IN (SELECT local_id FROM (SELECT local_id,ROW_NUMBER() OVER (PARTITION BY server_id ORDER BY local_id) AS quote_rank FROM {table_name} WHERE server_id IN ({placeholders})) WHERE quote_rank<=2) ORDER BY server_id,local_id");
1044 let mut statement = c.prepare(&sql)?;
1045 #[cfg(test)]
1046 batch_lookup_tests::record_batch_query();
1047 let mut rows = statement.query(rusqlite::params_from_iter(servers))?;
1048 while let Some(row) = rows.next()? {
1049 let server: i64 = row.get("server_id")?;
1050 let count = counts.entry((chat, server)).or_default();
1051 if *count == 2 {
1052 continue;
1053 }
1054 let local: i64 = row.get("local_id")?;
1055 if local <= 0 {
1056 return Err(ProfileError::InvalidQuery.into());
1057 }
1058 let id = SourceMessageId {
1059 database: schema.database.clone(),
1060 table: table.name.clone(),
1061 local_id: local.to_string(),
1062 };
1063 let mut message =
1064 self.message_from_fields(raw(row)?, &id, own_username, &labels, &names)?;
1065 message.peer = Some(chat.into());
1066 visit(message)?;
1067 *count += 1;
1068 }
1069 }
1070 }
1071 Ok(())
1072 }
1073
1074 fn message_from_connection(
1075 &self,
1076 c: &Connection,
1077 id: &SourceMessageId,
1078 own_username: Option<&str>,
1079 labels: &BTreeMap<String, String>,
1080 names: &BTreeMap<i64, String>,
1081 ) -> Result<Message> {
1082 if !message_table(&id.table) || !shard(&id.database) {
1083 return Err(ProfileError::InvalidQuery);
1084 }
1085 if let Some(own) = own_username {
1086 validate_name(own)?;
1087 }
1088 let local = id
1089 .local_id
1090 .parse::<i64>()
1091 .map_err(|_| ProfileError::InvalidQuery)?;
1092 if local <= 0 || local.to_string() != id.local_id {
1093 return Err(ProfileError::InvalidQuery);
1094 }
1095 let table = self
1096 .schemas
1097 .iter()
1098 .find(|s| s.database == id.database)
1099 .and_then(|s| s.tables.iter().find(|t| t.name == id.table))
1100 .ok_or(ProfileError::InvalidQuery)?;
1101 if ![
1102 "local_id",
1103 "server_id",
1104 "local_type",
1105 "create_time",
1106 "real_sender_id",
1107 "message_content",
1108 "source",
1109 "WCDB_CT_message_content",
1110 "WCDB_CT_source",
1111 ]
1112 .iter()
1113 .all(|n| table.columns.iter().any(|c| c.name == *n))
1114 {
1115 return Err(ProfileError::UnsupportedSchema(id.database.clone()));
1116 }
1117 #[cfg(test)]
1118 batch_lookup_tests::record_point_query();
1119 let r = c
1120 .query_row(
1121 &format!(
1122 "SELECT * FROM {} WHERE local_id=?1",
1123 quote_identifier(&id.table)
1124 ),
1125 [local],
1126 raw,
1127 )
1128 .optional()?
1129 .ok_or(ProfileError::NotCached)?;
1130 self.message_from_fields(r, id, own_username, labels, names)
1131 }
1132
1133 fn message_from_fields(
1134 &self,
1135 r: RawFields,
1136 id: &SourceMessageId,
1137 own_username: Option<&str>,
1138 labels: &BTreeMap<String, String>,
1139 names: &BTreeMap<i64, String>,
1140 ) -> Result<Message> {
1141 self.message_from_fields_with_peer(r, id, own_username, labels, names, None)
1142 }
1143
1144 fn message_from_fields_with_peer(
1145 &self,
1146 r: RawFields,
1147 id: &SourceMessageId,
1148 own_username: Option<&str>,
1149 labels: &BTreeMap<String, String>,
1150 names: &BTreeMap<i64, String>,
1151 known_peer: Option<&str>,
1152 ) -> Result<Message> {
1153 let peer = if let Some(peer) = known_peer {
1154 Some(peer.to_owned())
1155 } else {
1156 let mut candidates = labels
1157 .keys()
1158 .cloned()
1159 .chain(names.values().cloned())
1160 .filter(|name| {
1161 format!("{:x}", Md5::digest(name.as_bytes()))
1162 .eq_ignore_ascii_case(&id.table[4..])
1163 })
1164 .collect::<BTreeSet<_>>();
1165 if candidates.len() == 1 {
1166 candidates.pop_first()
1167 } else {
1168 None
1169 }
1170 };
1171 let signed = num(&r, "server_id");
1172 let sender = num(&r, "real_sender_id").and_then(|n| names.get(&n).cloned());
1173 let outgoing = own_username.and_then(|own| sender.as_ref().map(|s| s == own));
1174 let local_type = num(&r, "local_type").ok_or(ProfileError::InvalidMetadata)?;
1175 Ok(Message {
1176 id: id.clone(),
1177 server_message_id: signed.filter(|v| *v != 0).map(|v| (v as u64).to_string()),
1178 server_id_signed: signed.map(|n| n.to_string()),
1179 peer,
1180 sender,
1181 outgoing,
1182 message_type: local_type & 0x7fff_ffff,
1183 local_type: local_type.to_string(),
1184 create_time: num(&r, "create_time").ok_or(ProfileError::InvalidMetadata)?,
1185 sort_seq: num(&r, "sort_seq").map(|n| n.to_string()),
1186 content: decode(
1187 &r,
1188 "message_content",
1189 "WCDB_CT_message_content",
1190 self.snapshot.options.max_blob_bytes,
1191 )?,
1192 source: decode(
1193 &r,
1194 "source",
1195 "WCDB_CT_source",
1196 self.snapshot.options.max_blob_bytes,
1197 )?,
1198 raw_fields: r,
1199 })
1200 }
1201 pub fn groups(&self, q: &ListQuery) -> Result<Page<Group>> {
1202 let n = limit(q.limit)?;
1203 let hash = query_hash("groups", &q.search)?;
1204 let after = match decode_cursor(q.cursor.as_deref(), self.generation(), &hash)? {
1205 None => String::new(),
1206 Some(CursorKey::Name { name }) => name,
1207 _ => return Err(ProfileError::InvalidCursor),
1208 };
1209 let c = self.snapshot.open(self.role("contact.db", "groups")?)?;
1210 let mut s=c.prepare("SELECT * FROM chat_room WHERE username>?1 AND (?2 IS NULL OR instr(username,?2)>0) ORDER BY username LIMIT ?3")?;
1211 let records = s
1212 .query_map(params![after, q.search, n + 1], raw)?
1213 .collect::<std::result::Result<Vec<_>, _>>()?;
1214 let labels = self.labels()?;
1215 let mut items = Vec::new();
1216 for r in records {
1217 let username = text(&r, "username").ok_or(ProfileError::InvalidMetadata)?;
1218 let id = num(&r, "id").ok_or(ProfileError::InvalidMetadata)?;
1219 let announcement = c
1220 .query_row(
1221 "SELECT username_,announcement_ FROM chat_room_info_detail WHERE room_id_=?1",
1222 [id],
1223 |r| Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?)),
1224 )
1225 .optional()?;
1226 if announcement.as_ref().is_some_and(|a| a.0 != username) {
1227 return Err(ProfileError::InvalidMetadata);
1228 }
1229 items.push(Group {
1230 label: labels
1231 .get(&username)
1232 .cloned()
1233 .unwrap_or_else(|| username.clone()),
1234 username,
1235 local_id: id.to_string(),
1236 owner: text(&r, "owner"),
1237 announcement: announcement.and_then(|v| v.1),
1238 raw_fields: r,
1239 });
1240 }
1241 self.page(items, n, &hash, |v| CursorKey::Name {
1242 name: v.username.clone(),
1243 })
1244 }
1245 pub fn group_mention_permissions(
1246 &self,
1247 room: &str,
1248 own: Option<&str>,
1249 ) -> Result<GroupMentionPermissions> {
1250 validate_name(room)?;
1251 let c = self.snapshot.open(self.role("contact.db", "groups")?)?;
1252 let (id, owner, packed, membership) = c
1253 .query_row(
1254 "SELECT r.id,r.owner,r.ext_buffer,c.is_in_chat_room FROM chat_room r LEFT JOIN contact c ON c.username=r.username WHERE r.username=?1",
1255 [room],
1256 |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Option<String>>(1)?, r.get::<_, Option<Vec<u8>>>(2)?, r.get::<_, Option<i64>>(3)?)),
1257 )
1258 .optional()?
1259 .ok_or(ProfileError::NotCached)?;
1260 let administrator_ids = packed.as_deref().map(room_administrators).transpose()?;
1261 let can_mention_all = if membership == Some(2) {
1262 Some(false)
1264 } else if let Some(own) = own {
1265 validate_name(own)?;
1266 let is_member: bool = c.query_row(
1267 "SELECT EXISTS(SELECT 1 FROM chatroom_member m JOIN contact c ON c.id=m.member_id WHERE m.room_id=?1 AND c.username=?2)",
1268 params![id, own],
1269 |r| r.get(0),
1270 )?;
1271 if !is_member {
1272 None
1273 } else if owner.as_deref() == Some(own) {
1274 Some(true)
1275 } else {
1276 administrator_ids.as_ref().and_then(|ids| {
1277 if ids.iter().any(|id| id == own) {
1278 Some(true)
1279 } else {
1280 owner.as_ref().filter(|id| !id.is_empty()).map(|_| false)
1281 }
1282 })
1283 }
1284 } else {
1285 None
1286 };
1287 Ok(GroupMentionPermissions {
1288 administrator_ids,
1289 can_mention_all,
1290 })
1291 }
1292 pub fn group_members(&self, room: &str, q: &ListQuery) -> Result<Page<GroupMember>> {
1293 validate_name(room)?;
1294 let n = limit(q.limit)?;
1295 let hash = query_hash("members", &(room, &q.search))?;
1296 let after = match decode_cursor(q.cursor.as_deref(), self.generation(), &hash)? {
1297 None => String::new(),
1298 Some(CursorKey::Name { name }) => name,
1299 _ => return Err(ProfileError::InvalidCursor),
1300 };
1301 let c = self.snapshot.open(self.role("contact.db", "groups")?)?;
1302 let room_row = c
1303 .query_row(
1304 "SELECT id,ext_buffer FROM chat_room WHERE username=?1",
1305 [room],
1306 |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Option<Vec<u8>>>(1)?)),
1307 )
1308 .optional()?
1309 .ok_or(ProfileError::NotCached)?;
1310 let displays = room_displays(room_row.1.as_deref().unwrap_or(&[]))?;
1311 let mut sql_members = BTreeSet::new();
1312 let mut stmt=c.prepare("SELECT contact.username FROM chatroom_member JOIN contact ON contact.id=chatroom_member.member_id WHERE chatroom_member.room_id=?1")?;
1313 for row in stmt.query_map([room_row.0], |r| r.get::<_, String>(0))? {
1314 sql_members.insert(row?);
1315 if sql_members.len() > 100000 {
1316 return Err(ProfileError::ResourceLimit);
1317 }
1318 }
1319 if !displays.is_empty() && displays.keys().cloned().collect::<BTreeSet<_>>() != sql_members
1320 {
1321 return Err(ProfileError::InvalidMetadata);
1322 }
1323 let mut stmt=c.prepare("SELECT c.* FROM contact c JOIN chatroom_member m ON m.member_id=c.id WHERE m.room_id=?1 AND c.username>?2 AND (?3 IS NULL OR instr(c.username,?3)>0 OR instr(coalesce(c.nick_name,''),?3)>0 OR instr(coalesce(c.remark,''),?3)>0) ORDER BY c.username LIMIT ?4")?;
1324 let records = stmt
1325 .query_map(params![room_row.0, after, q.search, n + 1], raw)?
1326 .collect::<std::result::Result<Vec<_>, _>>()?;
1327 let mut items = Vec::new();
1328 for r in records {
1329 let contact = contact(r)?;
1330 let display = displays.get(&contact.username).cloned().flatten();
1331 items.push(group_member(contact, display));
1332 }
1333 self.page(items, n, &hash, |v| CursorKey::Name {
1334 name: v.username.clone(),
1335 })
1336 }
1337 pub fn avatar(&self, username: &str) -> Result<CachedBytes> {
1338 validate_name(username)?;
1339 let rel = self.role("head_image.db", "avatars")?;
1340 let c = self.snapshot.open(rel)?;
1341 let bytes = c
1342 .query_row(
1343 "SELECT image_buffer FROM head_image WHERE username=?1",
1344 [username],
1345 |r| r.get::<_, Vec<u8>>(0),
1346 )
1347 .optional()?
1348 .ok_or(ProfileError::NotCached)?;
1349 let (media_type, _, _) = super::images::validate_image(&bytes)?;
1350 Ok(CachedBytes {
1351 media_type,
1352 sha256: hex::encode(Sha256::digest(&bytes)),
1353 bytes,
1354 source_database: rel.into(),
1355 source_table: "head_image".into(),
1356 })
1357 }
1358 pub fn cached_voice(
1361 &self,
1362 database: &str,
1363 chat: &str,
1364 local_id: &str,
1365 server_id: Option<&str>,
1366 ) -> Result<CachedBytes> {
1367 validate_name(chat)?;
1368 let local: i64 = local_id.parse().map_err(|_| ProfileError::InvalidQuery)?;
1369 let sid = server_id
1370 .map(|s| s.parse::<u64>().map(|n| n as i64))
1371 .transpose()
1372 .map_err(|_| ProfileError::InvalidQuery)?;
1373 let c = self.snapshot.open(database)?;
1374 if !self
1375 .schemas
1376 .iter()
1377 .any(|s| s.database == database && s.supports("voice"))
1378 {
1379 return Err(ProfileError::UnsupportedSchema(database.into()));
1380 }
1381 let mut s=c.prepare("SELECT voice_data FROM VoiceInfo JOIN Name2Id n ON n.rowid=VoiceInfo.chat_name_id WHERE n.user_name=?1 AND VoiceInfo.local_id=?2 AND (?3 IS NULL OR VoiceInfo.svr_id=?3) AND length(voice_data)>0 LIMIT 2")?;
1382 let mut found = s
1383 .query_map(params![chat, local, sid], |r| r.get::<_, Vec<u8>>(0))?
1384 .collect::<std::result::Result<Vec<_>, _>>()?;
1385 if found.len() != 1 {
1386 return Err(if found.is_empty() {
1387 ProfileError::NotCached
1388 } else {
1389 ProfileError::InvalidQuery
1390 });
1391 }
1392 let bytes = found.remove(0);
1393 Ok(CachedBytes {
1394 media_type: "application/octet-stream".into(),
1395 sha256: hex::encode(Sha256::digest(&bytes)),
1396 bytes,
1397 source_database: database.into(),
1398 source_table: "VoiceInfo".into(),
1399 })
1400 }
1401 pub fn decode_dat(&self, bytes: &[u8]) -> Result<(Vec<u8>, crate::DatLayout)> {
1402 crate::decode_dat_v2(
1403 bytes,
1404 self.snapshot
1405 .keys
1406 .image_dat
1407 .as_ref()
1408 .ok_or(ProfileError::ImageKeyUnavailable)?,
1409 )
1410 }
1411}
1412fn message_order(m: &Message) -> (i64, &str, &str, i64) {
1413 (
1414 m.create_time,
1415 &m.id.database,
1416 &m.id.table,
1417 m.id.local_id.parse().unwrap_or(0),
1418 )
1419}
1420fn require_message_columns(table: &crate::TableSchema, database: &str) -> Result<()> {
1421 let required = [
1422 "local_id",
1423 "server_id",
1424 "local_type",
1425 "create_time",
1426 "real_sender_id",
1427 "message_content",
1428 "source",
1429 "WCDB_CT_message_content",
1430 "WCDB_CT_source",
1431 ];
1432 if required
1433 .iter()
1434 .all(|name| table.columns.iter().any(|c| c.name == *name))
1435 {
1436 Ok(())
1437 } else {
1438 Err(ProfileError::UnsupportedSchema(database.to_owned()))
1439 }
1440}
1441
1442pub(crate) fn protobuf_fields(b: &[u8]) -> Result<Vec<(u32, u8, &[u8])>> {
1443 fn var(b: &[u8], i: &mut usize) -> Result<u64> {
1444 let mut n = 0u64;
1445 for k in 0..10 {
1446 let x = *b.get(*i).ok_or(ProfileError::InvalidMetadata)?;
1447 *i += 1;
1448 if k == 9 && x > 1 {
1449 return Err(ProfileError::InvalidMetadata);
1450 }
1451 n |= u64::from(x & 127) << (7 * k);
1452 if x < 128 {
1453 return Ok(n);
1454 }
1455 }
1456 Err(ProfileError::InvalidMetadata)
1457 }
1458 if b.len() > 16 * 1024 * 1024 {
1459 return Err(ProfileError::ResourceLimit);
1460 }
1461 let mut out = Vec::new();
1462 let mut i = 0;
1463 while i < b.len() {
1464 let tag = var(b, &mut i)?;
1465 let n = (tag >> 3) as u32;
1466 let w = (tag & 7) as u8;
1467 if tag >> 3 == 0 || tag >> 3 >= 1 << 29 {
1468 return Err(ProfileError::InvalidMetadata);
1469 }
1470 let (start, end) = match w {
1471 0 => {
1472 let start = i;
1473 var(b, &mut i)?;
1474 (start, i)
1475 }
1476 1 => (i, i.checked_add(8).ok_or(ProfileError::InvalidMetadata)?),
1477 5 => (i, i.checked_add(4).ok_or(ProfileError::InvalidMetadata)?),
1478 2 => {
1479 let z =
1480 usize::try_from(var(b, &mut i)?).map_err(|_| ProfileError::InvalidMetadata)?;
1481 (i, i.checked_add(z).ok_or(ProfileError::InvalidMetadata)?)
1482 }
1483 _ => return Err(ProfileError::InvalidMetadata),
1484 };
1485 let value = b.get(start..end).ok_or(ProfileError::InvalidMetadata)?;
1486 i = end;
1487 out.push((n, w, value));
1488 if out.len() > 100000 {
1489 return Err(ProfileError::ResourceLimit);
1490 }
1491 }
1492 Ok(out)
1493}
1494
1495fn room_administrators(b: &[u8]) -> Result<Vec<String>> {
1496 let mut ids = Vec::new();
1497 let mut seen = BTreeSet::new();
1498 for (n, w, v) in protobuf_fields(b)? {
1501 if n != 5 {
1502 continue;
1503 }
1504 if w != 2 {
1505 return Err(ProfileError::InvalidMetadata);
1506 }
1507 let id = std::str::from_utf8(v).map_err(|_| ProfileError::InvalidMetadata)?;
1508 validate_name(id)?;
1509 if !seen.insert(id.to_owned()) || ids.len() >= 10000 {
1510 return Err(ProfileError::InvalidMetadata);
1511 }
1512 ids.push(id.to_owned());
1513 }
1514 Ok(ids)
1515}
1516
1517fn room_displays(b: &[u8]) -> Result<BTreeMap<String, Option<String>>> {
1518 let mut out = BTreeMap::new();
1519 for (n, w, v) in protobuf_fields(b)? {
1520 if n != 1 {
1521 continue;
1522 }
1523 if w != 2 {
1524 return Err(ProfileError::InvalidMetadata);
1525 }
1526 let fields = protobuf_fields(v)?;
1527 let scalar = |number| -> Result<Option<String>> {
1528 let mut values = fields.iter().filter(|(n, _, _)| *n == number);
1529 let one = values.next();
1530 if values.next().is_some() {
1531 return Err(ProfileError::InvalidMetadata);
1532 }
1533 one.map(|(_, w, v)| {
1534 if *w == 2 {
1535 String::from_utf8(v.to_vec()).map_err(|_| ProfileError::InvalidMetadata)
1536 } else {
1537 Err(ProfileError::InvalidMetadata)
1538 }
1539 })
1540 .transpose()
1541 };
1542 let username = scalar(1)?.ok_or(ProfileError::InvalidMetadata)?;
1543 let display = scalar(2)?;
1544 if out.insert(username, display).is_some() {
1545 return Err(ProfileError::InvalidMetadata);
1546 }
1547 }
1548 Ok(out)
1549}
1550
1551#[cfg(test)]
1552mod batch_lookup_tests {
1553 use super::*;
1554 use std::cell::Cell;
1555
1556 thread_local! {
1557 static BATCH_QUERIES: Cell<usize> = const { Cell::new(0) };
1558 static POINT_QUERIES: Cell<usize> = const { Cell::new(0) };
1559 }
1560 pub(super) fn record_batch_query() {
1561 BATCH_QUERIES.with(|n| n.set(n.get() + 1));
1562 }
1563 pub(super) fn record_point_query() {
1564 POINT_QUERIES.with(|n| n.set(n.get() + 1));
1565 }
1566
1567 fn fixture() -> (tempfile::TempDir, ProfileReader) {
1568 let temp = tempfile::tempdir().unwrap();
1569 let directory = temp.path().join("account/db_storage/message");
1570 std::fs::create_dir_all(&directory).unwrap();
1571 let table = format!("Msg_{:x}", Md5::digest(b"peer"));
1572 for shard in 0..2 {
1573 let db = Connection::open(directory.join(format!("message_{shard}.db"))).unwrap();
1574 db.execute_batch("CREATE TABLE Name2Id(user_name TEXT PRIMARY KEY,is_session INTEGER)")
1575 .unwrap();
1576 for user in if shard == 0 {
1577 ["self", "peer"]
1578 } else {
1579 ["peer", "self"]
1580 } {
1581 db.execute("INSERT INTO Name2Id VALUES(?1,1)", [user])
1582 .unwrap();
1583 }
1584 db.execute_batch(&format!("CREATE TABLE {table}(local_id INTEGER PRIMARY KEY,server_id INTEGER,local_type INTEGER,create_time INTEGER,real_sender_id INTEGER,message_content TEXT,source TEXT,WCDB_CT_message_content INTEGER,WCDB_CT_source INTEGER)")).unwrap();
1585 for local in 1..=25 {
1586 db.execute(
1587 &format!("INSERT INTO {table} VALUES(?1,?2,1,100,?3,?4,'',0,0)"),
1588 params![
1589 local,
1590 shard * 25 + local,
1591 if shard == 0 { 2 } else { 1 },
1592 format!("Shard {shard} row {local}")
1593 ],
1594 )
1595 .unwrap();
1596 }
1597 db.execute(
1598 &format!("INSERT INTO {table} VALUES(1000,-1,1,100,1,'Unsigned ID','',0,0)"),
1599 [],
1600 )
1601 .unwrap();
1602 for local in 2000..2003 {
1603 db.execute(
1604 &format!("INSERT INTO {table} VALUES(?1,99,1,100,1,'Duplicate','',0,0)"),
1605 [local],
1606 )
1607 .unwrap();
1608 }
1609 }
1610 let reader = ProfileReader::open(
1611 ProfileLocation::new(temp.path().join("account")).unwrap(),
1612 KeyFile::empty("account"),
1613 ReadOptions {
1614 allow_plaintext: true,
1615 stopped_or_consistent_snapshot: true,
1616 ..Default::default()
1617 },
1618 )
1619 .unwrap();
1620 (temp, reader)
1621 }
1622
1623 #[test]
1624 fn notification_preferences_use_one_query_for_a_full_page() {
1625 let (temp, original) = fixture();
1626 drop(original);
1627 let directory = temp.path().join("account/db_storage/contact");
1628 std::fs::create_dir_all(&directory).unwrap();
1629 let db = Connection::open(directory.join("contact.db")).unwrap();
1630 db.execute_batch(include_str!("layout/contact.db.sql"))
1631 .unwrap();
1632 let chats: Vec<_> = (0..200).map(|n| format!("peer_{n}")).collect();
1633 for (id, chat) in chats.iter().enumerate() {
1634 db.execute(
1635 "INSERT INTO contact(id,username,flag,local_type,delete_flag) VALUES(?1,?2,?3,1,0)",
1636 params![id, chat, if id % 2 == 0 { 3 } else { 515 }],
1637 )
1638 .unwrap();
1639 }
1640 let reader = ProfileReader::open(
1641 ProfileLocation::new(temp.path().join("account")).unwrap(),
1642 KeyFile::empty("account"),
1643 ReadOptions {
1644 allow_plaintext: true,
1645 stopped_or_consistent_snapshot: true,
1646 ..Default::default()
1647 },
1648 )
1649 .unwrap();
1650 BATCH_QUERIES.with(|n| n.set(0));
1651 let states = reader.thread_muted(&chats).unwrap();
1652 assert_eq!(states.len(), 200);
1653 for (id, chat) in chats.iter().enumerate() {
1654 assert_eq!(states[chat], Some(id % 2 != 0));
1655 }
1656 BATCH_QUERIES.with(|n| assert_eq!(n.get(), 1));
1657 assert!(reader.thread_muted(&[]).unwrap().is_empty());
1658 assert!(matches!(
1659 reader.thread_muted(&vec!["peer".into(); 201]),
1660 Err(ProfileError::InvalidQuery)
1661 ));
1662 }
1663
1664 #[test]
1665 fn pin_preferences_use_one_query_and_keep_incomplete_or_special_peers_unknown() {
1666 let (temp, original) = fixture();
1667 drop(original);
1668 let directory = temp.path().join("account/db_storage/contact");
1669 std::fs::create_dir_all(&directory).unwrap();
1670 let db = Connection::open(directory.join("contact.db")).unwrap();
1671 db.execute_batch(include_str!("layout/contact.db.sql"))
1672 .unwrap();
1673 let chats: Vec<_> = (0..200)
1674 .map(|n| {
1675 if n % 2 == 0 {
1676 format!("peer_{n}")
1677 } else {
1678 format!("room_{n}@chatroom")
1679 }
1680 })
1681 .collect();
1682 for (id, chat) in chats.iter().enumerate() {
1683 let kind = if id % 2 == 0 { 1 } else { 2 };
1684 let flag = kind | 0x200 | if id % 4 < 2 { 0x800 } else { 0 };
1686 db.execute(
1687 "INSERT INTO contact(id,username,flag,local_type,delete_flag,is_in_chat_room) VALUES(?1,?2,?3,?4,0,1)",
1688 params![id, chat, flag, kind],
1689 )
1690 .unwrap();
1691 }
1692 db.execute_batch(
1693 "INSERT INTO contact(id,username,flag,local_type,delete_flag,is_in_chat_room) VALUES
1694 (201,'missing_flag',NULL,1,0,1),
1695 (202,'negative_flag',-1,1,0,1),
1696 (203,'wide_flag',4294967296,1,0,1),
1697 (204,'malformed_flag','not a flag',1,0,1),
1698 (205,'not_friend',2048,1,0,1),
1699 (206,'deleted',2049,1,1,1),
1700 (207,'wrong_type',2049,7,0,1),
1701 (208,'left@chatroom',2050,2,0,2),
1702 (209,'not_group@chatroom',2049,1,0,1),
1703 (210,'filehelper',2049,1,0,1),
1704 (211,'weixin',2049,1,0,1),
1705 (212,'gh_service',2049,1,0,1),
1706 (213,'notification_messages',2049,1,0,1);",
1707 )
1708 .unwrap();
1709 let reader = ProfileReader::open(
1710 ProfileLocation::new(temp.path().join("account")).unwrap(),
1711 KeyFile::empty("account"),
1712 ReadOptions {
1713 allow_plaintext: true,
1714 stopped_or_consistent_snapshot: true,
1715 ..Default::default()
1716 },
1717 )
1718 .unwrap();
1719 BATCH_QUERIES.with(|n| n.set(0));
1720 let states = reader.thread_pinned(&chats).unwrap();
1721 assert_eq!(states.len(), 200);
1722 for (id, chat) in chats.iter().enumerate() {
1723 assert_eq!(states[chat], Some(id % 4 < 2));
1724 }
1725 BATCH_QUERIES.with(|n| assert_eq!(n.get(), 1));
1726 let unknown = [
1727 "missing",
1728 "missing_flag",
1729 "negative_flag",
1730 "wide_flag",
1731 "malformed_flag",
1732 "not_friend",
1733 "deleted",
1734 "wrong_type",
1735 "left@chatroom",
1736 "not_group@chatroom",
1737 "filehelper",
1738 "weixin",
1739 "gh_service",
1740 "notification_messages",
1741 ]
1742 .map(str::to_owned);
1743 let states = reader.thread_pinned(&unknown).unwrap();
1744 for name in unknown {
1745 assert_eq!(states.get(&name).copied().flatten(), None, "{name}");
1746 }
1747 assert!(reader.thread_pinned(&[]).unwrap().is_empty());
1748 assert!(matches!(
1749 reader.thread_pinned(&vec!["peer".into(); 201]),
1750 Err(ProfileError::InvalidQuery)
1751 ));
1752 }
1753
1754 fn collect(
1755 reader: &ProfileReader,
1756 targets: &[(String, String)],
1757 own: Option<&str>,
1758 ) -> Result<Vec<Message>> {
1759 let mut messages = Vec::new();
1760 reader.visit_messages_by_server_ids::<ProfileError>(targets, own, |message| {
1761 messages.push(message);
1762 Ok(())
1763 })?;
1764 Ok(messages)
1765 }
1766
1767 #[test]
1768 fn sidebar_previews_use_one_compound_query_per_shard_for_a_full_page() {
1769 let (temp, original) = fixture();
1770 drop(original);
1771 let source = format!("Msg_{:x}", Md5::digest(b"peer"));
1772 let chats: Vec<_> = (0..50).map(|n| format!("peer_{n}")).collect();
1773 for shard in 0..2 {
1774 let db = Connection::open(
1775 temp.path()
1776 .join(format!("account/db_storage/message/message_{shard}.db")),
1777 )
1778 .unwrap();
1779 for chat in &chats {
1780 let table = format!("Msg_{:x}", Md5::digest(chat.as_bytes()));
1781 db.execute_batch(&format!("CREATE TABLE {table} AS SELECT * FROM {source}; CREATE INDEX {table}_time ON {table}(create_time DESC,local_id DESC)")).unwrap();
1782 }
1783 let table = format!("Msg_{:x}", Md5::digest(chats[0].as_bytes()));
1784 db.execute_batch(&format!("ALTER TABLE {table} ADD COLUMN __agent_wechat_preview BLOB; UPDATE {table} SET __agent_wechat_preview=X'0102'")).unwrap();
1785 }
1786 let reader = ProfileReader::open(
1787 ProfileLocation::new(temp.path().join("account")).unwrap(),
1788 KeyFile::empty("account"),
1789 ReadOptions {
1790 allow_plaintext: true,
1791 stopped_or_consistent_snapshot: true,
1792 ..Default::default()
1793 },
1794 )
1795 .unwrap();
1796 BATCH_QUERIES.with(|n| n.set(0));
1797 POINT_QUERIES.with(|n| n.set(0));
1798 let latest = reader.latest_messages(&chats, Some("self")).unwrap();
1799 assert_eq!(latest.len(), chats.len());
1800 assert_eq!(BATCH_QUERIES.with(Cell::get), 2);
1801 assert_eq!(POINT_QUERIES.with(Cell::get), 0);
1802 for chat in &chats {
1803 let expected = reader.message(&latest[chat].id, Some("self")).unwrap();
1804 assert_eq!(latest[chat].raw_fields, expected.raw_fields);
1805 assert_eq!(latest[chat].sender, expected.sender);
1806 }
1807 assert!(!latest[&chats[1]]
1808 .raw_fields
1809 .contains_key("__agent_wechat_preview"));
1810 }
1811
1812 #[test]
1813 fn a_page_uses_one_bulk_lookup_per_shard_and_no_original_point_reads() {
1814 let (_temp, reader) = fixture();
1815 let targets: Vec<_> = (1..=50).map(|n| ("peer".into(), n.to_string())).collect();
1816 BATCH_QUERIES.with(|n| n.set(0));
1817 POINT_QUERIES.with(|n| n.set(0));
1818 let batch = collect(&reader, &targets, Some("self")).unwrap();
1819 assert_eq!(BATCH_QUERIES.with(Cell::get), 2);
1820 assert_eq!(POINT_QUERIES.with(Cell::get), 0);
1821 assert_eq!(batch.len(), 50);
1822 assert!(batch
1823 .iter()
1824 .all(|m| m.sender.as_deref() == Some("peer") && m.outgoing == Some(false)));
1825 let individual: Vec<_> = targets
1826 .iter()
1827 .flat_map(|(thread, server)| {
1828 reader
1829 .messages_by_server_id(thread, server, Some("self"))
1830 .unwrap()
1831 })
1832 .collect();
1833 assert_eq!(POINT_QUERIES.with(Cell::get), 50);
1834 let by_id = |messages: Vec<Message>| {
1835 messages
1836 .into_iter()
1837 .map(|m| (format!("{:?}", m.id), serde_json::to_value(m).unwrap()))
1838 .collect::<BTreeMap<_, _>>()
1839 };
1840 assert_eq!(by_id(batch), by_id(individual));
1841 }
1842
1843 #[test]
1844 fn batch_lookup_keeps_unsigned_ids_duplicates_and_conversations_separate() {
1845 let (_temp, reader) = fixture();
1846 let targets = vec![
1847 ("peer".into(), u64::MAX.to_string()),
1848 ("peer".into(), "99".into()),
1849 ("peer".into(), "99".into()),
1850 ("other".into(), "1".into()),
1851 ("peer".into(), "99999".into()),
1852 ];
1853 let found = collect(&reader, &targets, None).unwrap();
1854 assert_eq!(found.len(), 4);
1855 assert_eq!(
1856 found
1857 .iter()
1858 .filter(|m| m.server_message_id.as_deref() == Some("99"))
1859 .count(),
1860 2
1861 );
1862 assert_eq!(
1863 found
1864 .iter()
1865 .filter(|m| m.server_message_id.as_deref() == Some(u64::MAX.to_string().as_str()))
1866 .count(),
1867 2
1868 );
1869 assert!(found.iter().all(|m| m.peer.as_deref() == Some("peer")));
1870 for invalid in ["0", "01", "-1", "18446744073709551616"] {
1871 assert!(matches!(
1872 collect(&reader, &[("peer".into(), invalid.into())], None),
1873 Err(ProfileError::InvalidQuery)
1874 ));
1875 }
1876 assert!(matches!(
1877 collect(&reader, &vec![("peer".into(), "1".into()); 201], None),
1878 Err(ProfileError::InvalidQuery)
1879 ));
1880 assert!(collect(&reader, &[], None).unwrap().is_empty());
1881 }
1882
1883 #[test]
1884 fn batch_visitor_stops_on_a_consumer_error_without_buffering_more_originals() {
1885 let (_temp, reader) = fixture();
1886 let targets: Vec<_> = (1..=50).map(|n| ("peer".into(), n.to_string())).collect();
1887 let mut visited = 0;
1888 let error = reader.visit_messages_by_server_ids::<ProfileError>(&targets, None, |_| {
1889 visited += 1;
1890 Err(ProfileError::ResourceLimit)
1891 });
1892 assert!(matches!(error, Err(ProfileError::ResourceLimit)));
1893 assert_eq!(visited, 1);
1894 }
1895}