Skip to main content

wechat_profile/
projection.rs

1//! Explicit, lossless projections into the inspected ordinary schemas. Callers
2//! retain the accepted server intent and opaque values; this module never guesses
3//! status, compression, group roster completeness or shard rotation.
4use crate::{
5    schema::quote_identifier,
6    writer::{find_database, open_write},
7    *,
8};
9use base64::{engine::general_purpose::STANDARD, Engine};
10use md5::{Digest, Md5};
11use rusqlite::{params, types::Value, Connection, OptionalExtension};
12use serde::{Deserialize, Serialize};
13#[path = "business_projection.rs"]
14mod business_projection;
15pub use business_projection::*;
16
17pub(crate) fn ownership(
18    location: &ProfileLocation,
19    keys: &KeyFile,
20    options: &WriteOptions,
21) -> Result<()> {
22    if !options.exclusive_ownership_confirmed {
23        return Err(ProfileError::SnapshotRequired);
24    }
25    if options.durable_intent_id.is_empty() || options.durable_intent_id.len() > 256 {
26        return Err(ProfileError::InvalidQuery);
27    }
28    if location.account_directory() != keys.account_binding {
29        return Err(ProfileError::AccountMismatch);
30    }
31    Ok(())
32}
33pub(crate) fn identity(value: &str) -> Result<()> {
34    if value.is_empty() || value.len() > 512 || value.chars().any(char::is_control) {
35        Err(ProfileError::InvalidQuery)
36    } else {
37        Ok(())
38    }
39}
40pub(crate) fn map_id(c: &Connection, table: &str, column: &str, name: &str) -> Result<i64> {
41    identity(name)?;
42    let table = quote_identifier(table);
43    let column = quote_identifier(column);
44    c.execute(
45        &format!("INSERT INTO {table}({column})VALUES(?1)ON CONFLICT({column})DO NOTHING"),
46        [name],
47    )?;
48    c.query_row(
49        &format!("SELECT rowid FROM {table} WHERE {column}=?1"),
50        [name],
51        |r| r.get(0),
52    )
53    .map_err(Into::into)
54}
55pub(crate) fn bound(v: &SqlValue) -> Result<(Value, bool)> {
56    Ok(match v {
57        SqlValue::Null => (Value::Null, false),
58        SqlValue::Integer(s) => (
59            Value::Integer(s.parse().map_err(|_| ProfileError::InvalidMetadata)?),
60            false,
61        ),
62        SqlValue::Real(s) => {
63            let f: f64 = s.parse().map_err(|_| ProfileError::InvalidMetadata)?;
64            if !f.is_finite() {
65                return Err(ProfileError::InvalidMetadata);
66            }
67            (Value::Real(f), false)
68        }
69        SqlValue::Text(s) => {
70            if s.len() > 4 * 1024 * 1024 {
71                return Err(ProfileError::ResourceLimit);
72            }
73            (Value::Text(s.clone()), false)
74        }
75        SqlValue::TextBytes(s) | SqlValue::Blob(s) => {
76            let b = STANDARD
77                .decode(s)
78                .map_err(|_| ProfileError::InvalidMetadata)?;
79            if b.len() > 4 * 1024 * 1024 {
80                return Err(ProfileError::ResourceLimit);
81            }
82            (Value::Blob(b), matches!(v, SqlValue::TextBytes(_)))
83        }
84    })
85}
86fn row_values(
87    c: &Connection,
88    table: &str,
89    columns: &[String],
90    key_column: &str,
91    key: &Value,
92) -> Result<Option<RawFields>> {
93    if columns.is_empty() {
94        return Err(ProfileError::InvalidMetadata);
95    }
96    let q = format!(
97        "SELECT {} FROM {} WHERE {}=?1",
98        columns
99            .iter()
100            .map(|s| quote_identifier(s))
101            .collect::<Vec<_>>()
102            .join(","),
103        quote_identifier(table),
104        quote_identifier(key_column)
105    );
106    c.query_row(&q, [key], crate::reader::raw)
107        .optional()
108        .map_err(Into::into)
109}
110pub(crate) fn check_known_table(c: &Connection, table: &str) -> Result<()> {
111    let relative = match table {
112        "contact"
113        | "biz_info"
114        | "chat_room"
115        | "chat_room_info_detail"
116        | "chatroom_member"
117        | "name2id" => "contact/contact.db",
118        "SessionTable" | "SessionUnreadListTable_1" | "SessionUnreadStatTable_1" => {
119            "session/session.db"
120        }
121        "MessageResourceInfo" | "MessageResourceDetail" | "ChatName2Id" | "SenderName2Id" => {
122            "message/message_resource.db"
123        }
124        "VoiceInfo" => "message/media_0.db",
125        "image_hardlink_info_v4"
126        | "file_hardlink_info_v4"
127        | "video_hardlink_info_v4"
128        | "dir2id"
129        | "db_info" => "hardlink/hardlink.db",
130        "head_image" => "head_image/head_image.db",
131        _ => return Err(ProfileError::WritesUnsupported),
132    };
133    let sql = crate::initialize::LAYOUTS
134        .iter()
135        .find(|x| x.0 == relative)
136        .ok_or(ProfileError::WritesUnsupported)?
137        .1;
138    let expected = Connection::open_in_memory()?;
139    expected.execute_batch(sql)?;
140    let expected = crate::schema::inspect(&expected, relative)?;
141    let actual = crate::schema::inspect(c, relative)?;
142    let a = actual
143        .tables
144        .iter()
145        .find(|t| t.name == table)
146        .ok_or_else(|| ProfileError::UnsupportedSchema(relative.into()))?;
147    let e = expected
148        .tables
149        .iter()
150        .find(|t| t.name == table)
151        .ok_or(ProfileError::InvalidMetadata)?;
152    let index = |t: &crate::TableSchema| {
153        t.indexes
154            .iter()
155            .map(|i| (i.name.clone(), i.unique, i.partial, i.columns.clone()))
156            .collect::<Vec<_>>()
157    };
158    // Supported writes name their columns. Physical order is not compatibility
159    // evidence; validate the same column definitions and constraints by name.
160    let columns_match = a.columns.len() == e.columns.len()
161        && a.columns.iter().all(|column| e.columns.contains(column));
162    if !columns_match || !a.triggers.is_empty() || index(a) != index(e) {
163        return Err(ProfileError::UnsupportedSchema(relative.into()));
164    }
165    Ok(())
166}
167fn apply_row(
168    c: &Connection,
169    table: &str,
170    key_column: &str,
171    key: SqlValue,
172    fields: &RawFields,
173    require_complete_new: bool,
174) -> Result<bool> {
175    check_known_table(c, table)?;
176    let report = crate::schema::inspect(c, "projection")?;
177    let t = report
178        .tables
179        .iter()
180        .find(|t| t.name == table)
181        .ok_or_else(|| ProfileError::UnsupportedSchema(table.into()))?;
182    if !t.triggers.is_empty() {
183        return Err(ProfileError::UnsupportedSchema(table.into()));
184    }
185    let columns = t.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>();
186    if fields.contains_key(key_column) || fields.keys().any(|f| !columns.contains(f)) {
187        return Err(ProfileError::InvalidMetadata);
188    }
189    let (key_value, _) = bound(&key)?;
190    let selected = fields.keys().cloned().collect::<Vec<_>>();
191    let existing = row_values(c, table, &selected, key_column, &key_value)?;
192    if existing.as_ref().is_some_and(|r| r == fields) {
193        return Ok(false);
194    }
195    if existing.is_none()
196        && require_complete_new
197        && columns
198            .iter()
199            .any(|col| col != key_column && !fields.contains_key(col))
200    {
201        return Err(ProfileError::InvalidMetadata);
202    }
203    let mut all = fields.clone();
204    all.insert(key_column.into(), key);
205    let mut values = vec![];
206    let mut expr = vec![];
207    let names = all.keys().cloned().collect::<Vec<_>>();
208    for v in all.values() {
209        let (value, as_text) = bound(v)?;
210        values.push(value);
211        let p = format!("?{}", values.len());
212        expr.push(if as_text {
213            format!("CAST({p} AS TEXT)")
214        } else {
215            p
216        });
217    }
218    let updates = names
219        .iter()
220        .filter(|n| n.as_str() != key_column)
221        .map(|n| format!("{}=excluded.{}", quote_identifier(n), quote_identifier(n)))
222        .collect::<Vec<_>>()
223        .join(",");
224    let sql = format!(
225        "INSERT INTO {}({})VALUES({})ON CONFLICT({})DO UPDATE SET {}",
226        quote_identifier(table),
227        names
228            .iter()
229            .map(|s| quote_identifier(s))
230            .collect::<Vec<_>>()
231            .join(","),
232        expr.join(","),
233        quote_identifier(key_column),
234        updates
235    );
236    c.execute(&sql, rusqlite::params_from_iter(values))?;
237    Ok(true)
238}
239/// Full scalar/BLOB contact data with id/username allocated from the destination
240/// name map. Existing rows accept patches; new rows require every remaining field.
241#[derive(Clone, Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase", deny_unknown_fields)]
243pub struct ContactProjection {
244    pub username: String,
245    pub fields: RawFields,
246}
247#[derive(Clone, Debug, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct AppliedContact {
250    pub local_id: i64,
251    pub changed: bool,
252}
253pub fn apply_contact_projection(
254    location: &ProfileLocation,
255    keys: &KeyFile,
256    input: &ContactProjection,
257    options: &WriteOptions,
258) -> Result<AppliedContact> {
259    ownership(location, keys, options)?;
260    identity(&input.username)?;
261    if input.fields.contains_key("id") || input.fields.contains_key("username") {
262        return Err(ProfileError::InvalidMetadata);
263    }
264    let db = find_database(location, "contact.db")?;
265    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
266    let tx = c.transaction()?;
267    let id = map_id(&tx, "name2id", "username", &input.username)?;
268    let mut fields = input.fields.clone();
269    fields.insert("username".into(), SqlValue::Text(input.username.clone()));
270    let changed = apply_row(
271        &tx,
272        "contact",
273        "id",
274        SqlValue::Integer(id.to_string()),
275        &fields,
276        true,
277    )?;
278    tx.commit()?;
279    Ok(AppliedContact {
280        local_id: id,
281        changed,
282    })
283}
284/// Apply only a received direct-contact mute bit. Missing or incomplete contact
285/// identities cannot establish the other flag bits and are left untouched.
286pub fn apply_contact_mute_patch(
287    location: &ProfileLocation,
288    keys: &KeyFile,
289    username: &str,
290    muted: bool,
291    options: &WriteOptions,
292) -> Result<Option<AppliedContact>> {
293    ownership(location, keys, options)?;
294    identity(username)?;
295    if username.ends_with("@chatroom") {
296        return Err(ProfileError::InvalidMetadata);
297    }
298    let db = find_database(location, "contact.db")?;
299    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
300    let tx = c.transaction()?;
301    check_known_table(&tx, "contact")?;
302    let existing = tx
303        .query_row(
304            "SELECT id,flag FROM contact WHERE username=?1 AND local_type=1 AND (delete_flag IS NULL OR delete_flag=0)",
305            [username],
306            |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Option<i64>>(1)?)),
307        )
308        .optional()?;
309    let Some((id, Some(flag))) = existing else {
310        return Ok(None);
311    };
312    let Some(flag) = u32::try_from(flag).ok().filter(|flag| flag & 1 != 0) else {
313        return Ok(None);
314    };
315    let flag = (flag & !0x200) | if muted { 0x200 } else { 0 };
316    let changed = apply_row(
317        &tx,
318        "contact",
319        "id",
320        SqlValue::Integer(id.to_string()),
321        &[("flag".into(), SqlValue::Integer(flag.to_string()))]
322            .into_iter()
323            .collect(),
324        true,
325    )?;
326    tx.commit()?;
327    Ok(Some(AppliedContact {
328        local_id: id,
329        changed,
330    }))
331}
332
333#[derive(Clone, Debug, Serialize)]
334#[serde(rename_all = "camelCase")]
335pub struct AppliedContactPin {
336    pub local_id: i64,
337    pub changed: bool,
338    pub session_projected: bool,
339    pub cross_database_atomic: bool,
340}
341
342/// Apply a received ordinary pin preference. The caller retains a stable
343/// transition timestamp and retries the same intent after a partial local write.
344/// Session and contact commits are separate; session-first publication lets a
345/// retry recognize an unfinished flag transition without rewinding newer activity.
346pub fn apply_contact_pin_patch(
347    location: &ProfileLocation,
348    keys: &KeyFile,
349    username: &str,
350    pinned: bool,
351    clear_folded_primary: bool,
352    sort_timestamp: u32,
353    options: &WriteOptions,
354) -> Result<Option<AppliedContactPin>> {
355    ownership(location, keys, options)?;
356    identity(username)?;
357    if sort_timestamp == 0 || clear_folded_primary && !pinned {
358        return Err(ProfileError::InvalidQuery);
359    }
360    let contact_db = find_database(location, "contact.db")?;
361    let session_db = find_database(location, "session.db")?;
362    let mut contact = open_write(location, &contact_db, keys, options.allow_plaintext)?;
363    let mut session = open_write(location, &session_db, keys, options.allow_plaintext)?;
364    // Validate both layouts before either database can be changed.
365    check_known_table(&contact, "contact")?;
366    check_known_table(&session, "SessionTable")?;
367    let contact_tx = contact.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
368    let session_tx = session.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
369    let existing = contact_tx
370        .query_row(
371            "SELECT id,flag,local_type,delete_flag,is_in_chat_room FROM contact WHERE username=?1",
372            [username],
373            crate::reader::raw,
374        )
375        .optional()?;
376    let Some(fields) = existing else {
377        return Ok(None);
378    };
379    let Some(flag) = crate::reader::num(&fields, "flag").and_then(|flag| u32::try_from(flag).ok())
380    else {
381        return Ok(None);
382    };
383    let ordinary = if username.ends_with("@chatroom") {
384        crate::reader::num(&fields, "local_type") == Some(2)
385            && crate::reader::num(&fields, "is_in_chat_room") == Some(1)
386            && flag & 2 != 0
387    } else {
388        !username.contains('@')
389            && username != "filehelper"
390            && !username.starts_with("weixin")
391            && !username.starts_with("gh_")
392            && ![
393                "brandsessionholder",
394                "brandservicesessionholder",
395                "qmessage",
396                "medianote",
397                "qqsync",
398                "lbsapp",
399                "shakeapp",
400                "floatbottle",
401                "masssendapp",
402                "appbrandcustomerservicemsg",
403                "service_officialaccounts",
404                "notifymessage",
405                "notification_messages",
406            ]
407            .contains(&username)
408            && crate::reader::num(&fields, "local_type") == Some(1)
409            && flag & 1 != 0
410    };
411    if !ordinary || !crate::reader::contact_not_deleted(&fields) {
412        return Ok(None);
413    }
414    let id = crate::reader::num(&fields, "id").ok_or(ProfileError::InvalidMetadata)?;
415    let existing_sort: Option<Option<i64>> = session_tx
416        .query_row(
417            "SELECT sort_timestamp FROM SessionTable WHERE username=?1",
418            [username],
419            |row| row.get(0),
420        )
421        .optional()?;
422    let Some(existing_sort) = existing_sort else {
423        return Ok(None);
424    };
425    let transition = (flag & 0x800 != 0) != pinned;
426    let flag = (flag & !0x800) | if pinned { 0x800 } else { 0 };
427    let flag = if clear_folded_primary {
428        flag & !0x10000080
429    } else {
430        flag
431    };
432    if transition && existing_sort.unwrap_or(0) < i64::from(sort_timestamp) {
433        session_tx.execute(
434            "UPDATE SessionTable SET sort_timestamp=?2 WHERE username=?1",
435            params![username, sort_timestamp],
436        )?;
437    }
438    let changed = apply_row(
439        &contact_tx,
440        "contact",
441        "id",
442        SqlValue::Integer(id.to_string()),
443        &[("flag".into(), SqlValue::Integer(flag.to_string()))]
444            .into_iter()
445            .collect(),
446        true,
447    )?;
448    session_tx.commit()?;
449    contact_tx.commit()?;
450    Ok(Some(AppliedContactPin {
451        local_id: id,
452        changed,
453        session_projected: true,
454        cross_database_atomic: false,
455    }))
456}
457/// Caller supplies the actual retained group fields; complete_roster is explicit.
458/// No absent member is deleted for an incomplete/delta roster.
459#[derive(Clone, Debug, Serialize, Deserialize)]
460#[serde(rename_all = "camelCase", deny_unknown_fields)]
461pub struct GroupProjection {
462    pub username: String,
463    pub fields: RawFields,
464    pub detail_fields: Option<RawFields>,
465    pub members: Vec<String>,
466    pub complete_roster: bool,
467}
468fn proto_var(data: &[u8], at: &mut usize) -> Result<u64> {
469    let mut n = 0;
470    for i in 0..10 {
471        let b = *data.get(*at).ok_or(ProfileError::InvalidMetadata)?;
472        *at += 1;
473        if i == 9 && b > 1 {
474            return Err(ProfileError::InvalidMetadata);
475        }
476        n |= u64::from(b & 127) << (i * 7);
477        if b & 128 == 0 {
478            return Ok(n);
479        }
480    }
481    Err(ProfileError::InvalidMetadata)
482}
483pub(crate) fn proto_fields(data: &[u8]) -> Result<Vec<(u64, u8, &[u8])>> {
484    let mut at = 0;
485    let mut out = vec![];
486    if data.len() > 1024 * 1024 {
487        return Err(ProfileError::ResourceLimit);
488    }
489    while at < data.len() {
490        let tag = proto_var(data, &mut at)?;
491        let wire = (tag & 7) as u8;
492        if tag >> 3 == 0 {
493            return Err(ProfileError::InvalidMetadata);
494        }
495        let start = at;
496        match wire {
497            0 => {
498                proto_var(data, &mut at)?;
499            }
500            1 => at = at.checked_add(8).ok_or(ProfileError::ResourceLimit)?,
501            2 => {
502                let n = usize::try_from(proto_var(data, &mut at)?)
503                    .map_err(|_| ProfileError::ResourceLimit)?;
504                let start = at;
505                at = at.checked_add(n).ok_or(ProfileError::ResourceLimit)?;
506                out.push((
507                    tag >> 3,
508                    wire,
509                    data.get(start..at).ok_or(ProfileError::InvalidMetadata)?,
510                ));
511                continue;
512            }
513            5 => at = at.checked_add(4).ok_or(ProfileError::ResourceLimit)?,
514            _ => return Err(ProfileError::InvalidMetadata),
515        }
516        out.push((
517            tag >> 3,
518            wire,
519            data.get(start..at).ok_or(ProfileError::InvalidMetadata)?,
520        ));
521        if out.len() > 65536 {
522            return Err(ProfileError::ResourceLimit);
523        }
524    }
525    Ok(out)
526}
527pub fn apply_group_projection(
528    location: &ProfileLocation,
529    keys: &KeyFile,
530    input: &GroupProjection,
531    options: &WriteOptions,
532) -> Result<()> {
533    ownership(location, keys, options)?;
534    identity(&input.username)?;
535    if !input.username.ends_with("@chatroom")
536        || input.members.len() > 10000
537        || input
538            .members
539            .iter()
540            .collect::<std::collections::BTreeSet<_>>()
541            .len()
542            != input.members.len()
543    {
544        return Err(ProfileError::InvalidMetadata);
545    }
546    let db = find_database(location, "contact.db")?;
547    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
548    let tx = c.transaction()?;
549    let id = map_id(&tx, "name2id", "username", &input.username)?;
550    let exists = tx
551        .query_row(
552            "SELECT id FROM contact WHERE id=?1 AND username=?2",
553            params![id, input.username],
554            |r| r.get::<_, i64>(0),
555        )
556        .optional()?;
557    if exists.is_none() {
558        return Err(ProfileError::InvalidMetadata);
559    }
560    let mut fields = input.fields.clone();
561    if fields.contains_key("id") || fields.contains_key("username") {
562        return Err(ProfileError::InvalidMetadata);
563    }
564    fields.insert("username".into(), SqlValue::Text(input.username.clone()));
565    if input.complete_roster {
566        let raw = match fields.get("ext_buffer") {
567            Some(SqlValue::Blob(v)) => STANDARD
568                .decode(v)
569                .map_err(|_| ProfileError::InvalidMetadata)?,
570            _ => return Err(ProfileError::InvalidMetadata),
571        };
572        let mut members = std::collections::BTreeSet::new();
573        for (_, wire, nested) in proto_fields(&raw)?.into_iter().filter(|f| f.0 == 1) {
574            if wire != 2 {
575                return Err(ProfileError::InvalidMetadata);
576            }
577            for (field, wire, v) in proto_fields(nested)? {
578                if field == 1 {
579                    if wire != 2 {
580                        return Err(ProfileError::InvalidMetadata);
581                    }
582                    members.insert(
583                        std::str::from_utf8(v)
584                            .map_err(|_| ProfileError::InvalidMetadata)?
585                            .to_owned(),
586                    );
587                }
588            }
589        }
590        if members != input.members.iter().cloned().collect() {
591            return Err(ProfileError::InvalidMetadata);
592        }
593    }
594    apply_row(
595        &tx,
596        "chat_room",
597        "id",
598        SqlValue::Integer(id.to_string()),
599        &fields,
600        true,
601    )?;
602    if let Some(fields) = &input.detail_fields {
603        apply_row(
604            &tx,
605            "chat_room_info_detail",
606            "room_id_",
607            SqlValue::Integer(id.to_string()),
608            fields,
609            true,
610        )?;
611    }
612    let mut member_ids = vec![];
613    for name in &input.members {
614        identity(name)?;
615        let member = tx
616            .query_row("SELECT id FROM contact WHERE username=?1", [name], |r| {
617                r.get::<_, i64>(0)
618            })
619            .optional()?
620            .ok_or(ProfileError::InvalidMetadata)?;
621        member_ids.push(member)
622    }
623    if input.complete_roster {
624        let existing = tx
625            .prepare("SELECT member_id FROM chatroom_member WHERE room_id=?1")?
626            .query_map([id], |r| r.get::<_, i64>(0))?
627            .collect::<std::result::Result<Vec<_>, _>>()?;
628        for old in existing {
629            if !member_ids.contains(&old) {
630                tx.execute(
631                    "DELETE FROM chatroom_member WHERE room_id=?1 AND member_id=?2",
632                    params![id, old],
633                )?;
634            }
635        }
636    }
637    for member in member_ids {
638        tx.execute("INSERT INTO chatroom_member(room_id,member_id)VALUES(?1,?2)ON CONFLICT(room_id,member_id)DO NOTHING",params![id,member])?;
639    }
640    tx.commit()?;
641    Ok(())
642}
643/// Exact stored columns supplied by a protocol/build adapter, excluding fields
644/// allocated/transformed by this destination (local_id, sender id, packed endpoints).
645#[derive(Clone, Debug, Serialize, Deserialize)]
646#[serde(rename_all = "camelCase", deny_unknown_fields)]
647pub struct MessageProjection {
648    pub peer: String,
649    pub sender: String,
650    pub from: String,
651    pub to: String,
652    pub server_message_id: String,
653    pub fields: RawFields,
654    /// Complete encoded fields other than endpoints1/2, including opaque fields.
655    pub packed_other_fields: Vec<u8>,
656}
657#[derive(Clone, Debug, Serialize)]
658#[serde(rename_all = "camelCase")]
659pub struct AppliedMessage {
660    pub source_id: SourceMessageId,
661    pub inserted: bool,
662}
663pub(crate) fn var_out(mut n: u64, out: &mut Vec<u8>) {
664    while n >= 128 {
665        out.push((n as u8 & 127) | 128);
666        n >>= 7
667    }
668    out.push(n as u8)
669}
670pub fn apply_message_projection(
671    location: &ProfileLocation,
672    keys: &KeyFile,
673    input: &MessageProjection,
674    options: &WriteOptions,
675) -> Result<AppliedMessage> {
676    ownership(location, keys, options)?;
677    for s in [&input.peer, &input.sender, &input.from, &input.to] {
678        identity(s)?
679    }
680    let sid = input
681        .server_message_id
682        .parse::<u64>()
683        .map_err(|_| ProfileError::InvalidMetadata)?;
684    if sid == 0 {
685        return Err(ProfileError::InvalidMetadata);
686    }
687    let table = format!("Msg_{:x}", Md5::digest(input.peer.as_bytes()));
688    for (f, _, _) in proto_fields(&input.packed_other_fields)? {
689        if f == 1 || f == 2 {
690            return Err(ProfileError::InvalidMetadata);
691        }
692    }
693    let db = find_database(location, "message_0.db")?;
694    crate::writer::require_single_message_shard(location)?;
695    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
696    let tx = c.transaction()?;
697    let sender = map_id(&tx, "Name2Id", "user_name", &input.sender)?;
698    let from = map_id(&tx, "Name2Id", "user_name", &input.from)?;
699    let to = map_id(&tx, "Name2Id", "user_name", &input.to)?;
700    tx.execute_batch(
701        &include_str!("layout/conversation.sql")
702            .replace("__MESSAGE_TABLE__", &table)
703            .replace("CREATE TABLE ", "CREATE TABLE IF NOT EXISTS ")
704            .replace("CREATE INDEX ", "CREATE INDEX IF NOT EXISTS "),
705    )?;
706    let schema = crate::writer::exact_columns(&tx, &db, &table, crate::writer::MESSAGE_COLUMNS)?;
707    let indexes = [
708        ("_SERVERID", vec!["server_id"]),
709        ("_SORTSEQ", vec!["sort_seq"]),
710        ("_TYPE_SEQ", vec!["local_type", "sort_seq"]),
711        ("_SENDERID", vec!["real_sender_id"]),
712    ];
713    if schema.indexes.len() != 4
714        || !indexes.iter().all(|(suffix, cols)| {
715            schema.indexes.iter().any(|i| {
716                i.name == format!("{table}{suffix}")
717                    && !i.unique
718                    && !i.partial
719                    && i.columns.iter().map(|x| x.as_deref()).collect::<Vec<_>>()
720                        == cols.iter().map(|x| Some(*x)).collect::<Vec<_>>()
721            })
722        })
723    {
724        return Err(ProfileError::UnsupportedSchema(db));
725    }
726    let mut fields = input.fields.clone();
727    for name in [
728        "local_id",
729        "server_id",
730        "real_sender_id",
731        "packed_info_data",
732    ] {
733        if fields.contains_key(name) {
734            return Err(ProfileError::InvalidMetadata);
735        }
736    }
737    let mut packed = vec![8];
738    var_out(from as u64, &mut packed);
739    packed.push(16);
740    var_out(to as u64, &mut packed);
741    packed.extend(&input.packed_other_fields);
742    fields.insert(
743        "server_id".into(),
744        SqlValue::Integer((sid as i64).to_string()),
745    );
746    fields.insert(
747        "real_sender_id".into(),
748        SqlValue::Integer(sender.to_string()),
749    );
750    fields.insert(
751        "packed_info_data".into(),
752        SqlValue::Blob(STANDARD.encode(packed)),
753    );
754    let ids = tx
755        .prepare(&format!(
756            "SELECT local_id FROM {} WHERE server_id=?1",
757            quote_identifier(&table)
758        ))?
759        .query_map([sid as i64], |r| r.get::<_, i64>(0))?
760        .collect::<std::result::Result<Vec<_>, _>>()?;
761    if ids.len() > 1 {
762        return Err(ProfileError::InvalidMetadata);
763    }
764    let (local, inserted) = if let Some(id) = ids.first() {
765        let cols = fields.keys().cloned().collect::<Vec<_>>();
766        if row_values(&tx, &table, &cols, "local_id", &Value::Integer(*id))?.as_ref()
767            != Some(&fields)
768        {
769            return Err(ProfileError::InvalidMetadata);
770        }
771        (*id, false)
772    } else {
773        if fields.len() + 1 != schema.columns.len()
774            || schema
775                .columns
776                .iter()
777                .any(|col| col.name != "local_id" && !fields.contains_key(&col.name))
778        {
779            return Err(ProfileError::InvalidMetadata);
780        }
781        // No invented tie-breaker. The explicit adapter must provide a collision-free
782        // sort sequence, or resolve the collision under a separately proved policy.
783        let sort = match fields.get("sort_seq") {
784            Some(SqlValue::Integer(n)) => n
785                .parse::<i64>()
786                .map_err(|_| ProfileError::InvalidMetadata)?,
787            _ => return Err(ProfileError::InvalidMetadata),
788        };
789        let count: i64 = tx.query_row(
790            &format!(
791                "SELECT count(*) FROM {} WHERE sort_seq=?1",
792                quote_identifier(&table)
793            ),
794            [sort],
795            |r| r.get(0),
796        )?;
797        if count != 0 {
798            return Err(ProfileError::WritesUnsupported);
799        }
800        // Temporarily use the next AUTOINCREMENT through an explicit NULL key; SQLite
801        // itself performs allocation and preserves its sqlite_sequence namespace.
802        let names = fields.keys().cloned().collect::<Vec<_>>();
803        let mut vals = vec![];
804        let mut expr = vec![];
805        for v in fields.values() {
806            let (value, text) = bound(v)?;
807            vals.push(value);
808            let p = format!("?{}", vals.len());
809            expr.push(if text {
810                format!("CAST({p} AS TEXT)")
811            } else {
812                p
813            })
814        }
815        tx.execute(
816            &format!(
817                "INSERT INTO {}({})VALUES({})",
818                quote_identifier(&table),
819                names
820                    .iter()
821                    .map(|s| quote_identifier(s))
822                    .collect::<Vec<_>>()
823                    .join(","),
824                expr.join(",")
825            ),
826            rusqlite::params_from_iter(vals),
827        )?;
828        (tx.last_insert_rowid(), true)
829    };
830    tx.commit()?;
831    Ok(AppliedMessage {
832        source_id: SourceMessageId {
833            database: db,
834            table,
835            local_id: local.to_string(),
836        },
837        inserted,
838    })
839}
840/// Explicit session fields. New sessions require a complete row; patches retain
841/// unrelated drafts/read metadata. Caller determines actual native values.
842pub fn apply_session_projection(
843    location: &ProfileLocation,
844    keys: &KeyFile,
845    peer: &str,
846    fields: &RawFields,
847    options: &WriteOptions,
848) -> Result<bool> {
849    ownership(location, keys, options)?;
850    identity(peer)?;
851    let db = find_database(location, "session.db")?;
852    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
853    let tx = c.transaction()?;
854    map_id(&tx, "Name2Id", "user_name", peer)?;
855    let changed = apply_row(
856        &tx,
857        "SessionTable",
858        "username",
859        SqlValue::Text(peer.into()),
860        fields,
861        true,
862    )?;
863    tx.commit()?;
864    Ok(changed)
865}
866
867/// Sparse contact update. Existing rows preserve every omitted field. A new row
868/// starts with a zero deletion flag; other omitted nullable/no-default columns
869/// become SQL NULL. Explicit input values take precedence over these defaults.
870pub fn apply_contact_patch(
871    location: &ProfileLocation,
872    keys: &KeyFile,
873    input: &ContactProjection,
874    options: &WriteOptions,
875) -> Result<AppliedContact> {
876    ownership(location, keys, options)?;
877    identity(&input.username)?;
878    let db = find_database(location, "contact.db")?;
879    let c = open_write(location, &db, keys, options.allow_plaintext)?;
880    check_known_table(&c, "contact")?;
881    let existing = c
882        .query_row(
883            "SELECT id FROM contact WHERE username=?1",
884            [&input.username],
885            |r| r.get::<_, i64>(0),
886        )
887        .optional()?;
888    if let Some(id) = existing.filter(|_| input.fields.is_empty()) {
889        return Ok(AppliedContact {
890            local_id: id,
891            changed: false,
892        });
893    }
894    let mut input = input.clone();
895    if existing.is_none() {
896        let mut fields = nullable_fields(&c, &db, "contact", &["id", "username"])?;
897        // A newly projected contact is not deleted unless the received update
898        // supplies a deletion value. Existing rows retain their saved state.
899        fields.insert("delete_flag".into(), SqlValue::Integer("0".into()));
900        fields.extend(input.fields);
901        input.fields = fields;
902    }
903    drop(c);
904    apply_contact_projection(location, keys, &input, options)
905}
906fn nullable_fields(
907    c: &Connection,
908    database: &str,
909    table: &str,
910    exclude: &[&str],
911) -> Result<RawFields> {
912    let report = crate::schema::inspect(c, database)?;
913    let t = report
914        .tables
915        .iter()
916        .find(|t| t.name == table)
917        .ok_or(ProfileError::InvalidMetadata)?;
918    let mut fields = RawFields::new();
919    for col in &t.columns {
920        if exclude.contains(&col.name.as_str()) {
921            continue;
922        }
923        if col.not_null || col.default_sql.is_some() {
924            return Err(ProfileError::WritesUnsupported);
925        }
926        fields.insert(col.name.clone(), SqlValue::Null);
927    }
928    Ok(fields)
929}
930/// Sparse group updates use the same explicit SQL NULL policy for a new row.
931/// Existing owner/opaque buffers are preserved unless specifically supplied.
932pub fn apply_group_patch(
933    location: &ProfileLocation,
934    keys: &KeyFile,
935    input: &GroupProjection,
936    options: &WriteOptions,
937) -> Result<()> {
938    ownership(location, keys, options)?;
939    let db = find_database(location, "contact.db")?;
940    let c = open_write(location, &db, keys, options.allow_plaintext)?;
941    check_known_table(&c, "chat_room")?;
942    let exists = c
943        .query_row(
944            "SELECT id FROM chat_room WHERE username=?1",
945            [&input.username],
946            |r| r.get::<_, i64>(0),
947        )
948        .optional()?
949        .is_some();
950    let mut input = input.clone();
951    if !exists {
952        let mut fields = nullable_fields(&c, &db, "chat_room", &["id", "username"])?;
953        fields.extend(input.fields);
954        input.fields = fields;
955    }
956    if let Some(detail) = &input.detail_fields {
957        check_known_table(&c, "chat_room_info_detail")?;
958        let exists = c
959            .query_row(
960                "SELECT room_id_ FROM chat_room_info_detail WHERE username_=?1",
961                [&input.username],
962                |r| r.get::<_, i64>(0),
963            )
964            .optional()?
965            .is_some();
966        if !exists {
967            let mut fields = nullable_fields(&c, &db, "chat_room_info_detail", &["room_id_"])?;
968            fields.extend(detail.clone());
969            input.detail_fields = Some(fields);
970        }
971    }
972    drop(c);
973    apply_group_projection(location, keys, &input, options)
974}
975
976#[derive(Clone, Debug, Serialize, Deserialize)]
977#[serde(rename_all = "camelCase", deny_unknown_fields)]
978pub struct NativeMemberPatch {
979    pub username: String,
980    pub display: Option<String>,
981    #[serde(default)]
982    pub flags: Option<u32>,
983    #[serde(default)]
984    pub opaque: Option<Vec<u8>>,
985}
986#[derive(Clone, Debug, Serialize, Deserialize)]
987#[serde(rename_all = "camelCase", deny_unknown_fields)]
988pub struct NativeRosterPatch {
989    pub username: String,
990    pub members: Vec<NativeMemberPatch>,
991    pub complete_roster: bool,
992    /// None preserves the saved list. Some replaces it, including an empty list.
993    #[serde(default)]
994    pub administrator_ids: Option<Vec<String>>,
995}
996
997/// Effects established for ordinary Linux group controls. This writer never
998/// deletes chat history and never treats a membership delta as a full roster.
999pub enum NativeGroupEdit<'a> {
1000    RemoveMembers(&'a [String]),
1001    Leave {
1002        own: &'a str,
1003    },
1004    MyName {
1005        own: &'a str,
1006        display: &'a str,
1007    },
1008    Notifications {
1009        muted: bool,
1010        clear_minimized: bool,
1011        own: &'a str,
1012    },
1013    Unfold {
1014        own: String,
1015    },
1016    ClearAnnouncement,
1017}
1018
1019pub fn apply_native_group_edit(
1020    location: &ProfileLocation,
1021    keys: &KeyFile,
1022    room: &str,
1023    edit: NativeGroupEdit<'_>,
1024    options: &WriteOptions,
1025) -> Result<()> {
1026    ownership(location, keys, options)?;
1027    identity(room)?;
1028    if !room.ends_with("@chatroom") {
1029        return Err(ProfileError::InvalidMetadata);
1030    }
1031    let db = find_database(location, "contact.db")?;
1032    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
1033    for table in [
1034        "contact",
1035        "chat_room",
1036        "chatroom_member",
1037        "chat_room_info_detail",
1038    ] {
1039        check_known_table(&c, table)?;
1040    }
1041    let tx = c.transaction()?;
1042    let (id, old): (i64, Option<Vec<u8>>) = tx.query_row(
1043        "SELECT id,ext_buffer FROM chat_room WHERE username=?1",
1044        [room],
1045        |r| Ok((r.get(0)?, r.get(1)?)),
1046    )?;
1047    let mut packed = vec![];
1048    let mut remove = Vec::new();
1049    let mut seen = std::collections::BTreeSet::new();
1050    let mut matched_name = false;
1051    let unfold_own = match &edit {
1052        NativeGroupEdit::Notifications {
1053            own,
1054            clear_minimized: true,
1055            ..
1056        } => Some(*own),
1057        NativeGroupEdit::Unfold { own } => {
1058            identity(own)?;
1059            Some(own.as_str())
1060        }
1061        _ => None,
1062    };
1063    match &edit {
1064        NativeGroupEdit::RemoveMembers(names) => remove.extend_from_slice(names),
1065        NativeGroupEdit::Leave { own } => remove.push((*own).to_owned()),
1066        _ => {}
1067    }
1068    for name in &remove {
1069        identity(name)?;
1070    }
1071    for chunk in proto_chunks(old.as_deref().unwrap_or_default())? {
1072        if chunk.field != 1 {
1073            packed.extend_from_slice(chunk.raw);
1074            continue;
1075        }
1076        if chunk.wire != 2 {
1077            return Err(ProfileError::InvalidMetadata);
1078        }
1079        let name = member_name(chunk.value)?;
1080        if !seen.insert(name.clone()) {
1081            return Err(ProfileError::InvalidMetadata);
1082        }
1083        if remove.contains(&name) {
1084            continue;
1085        }
1086        let updated = match &edit {
1087            NativeGroupEdit::MyName { own, display } if name == *own => {
1088                matched_name = true;
1089                Some(patch_member(
1090                    Some(chunk.value),
1091                    &NativeMemberPatch {
1092                        flags: None,
1093                        opaque: None,
1094                        username: name,
1095                        display: Some((*display).into()),
1096                    },
1097                )?)
1098            }
1099            _ if unfold_own == Some(name.as_str()) => {
1100                let mut member = vec![];
1101                let mut flag_seen = false;
1102                for field in proto_chunks(chunk.value)? {
1103                    if field.field == 3 {
1104                        if field.wire != 0 || flag_seen {
1105                            return Err(ProfileError::InvalidMetadata);
1106                        }
1107                        flag_seen = true;
1108                        let mut at = 0;
1109                        let value = proto_var(field.value, &mut at)?;
1110                        var_out(3 << 3, &mut member);
1111                        var_out(value & !0x2000, &mut member);
1112                    } else {
1113                        member.extend_from_slice(field.raw);
1114                    }
1115                }
1116                Some(member)
1117            }
1118            _ => None,
1119        };
1120        if let Some(member) = updated {
1121            proto_bytes(1, &member, &mut packed);
1122        } else {
1123            packed.extend_from_slice(chunk.raw);
1124        }
1125    }
1126    if matches!(edit, NativeGroupEdit::MyName { .. }) && !matched_name {
1127        return Err(ProfileError::InvalidMetadata);
1128    }
1129    if packed != old.as_deref().unwrap_or_default() {
1130        tx.execute(
1131            "UPDATE chat_room SET ext_buffer=?2 WHERE id=?1",
1132            rusqlite::params![id, packed],
1133        )?;
1134    }
1135    for name in &remove {
1136        tx.execute("DELETE FROM chatroom_member WHERE room_id=?1 AND member_id IN (SELECT id FROM contact WHERE username=?2)", rusqlite::params![id,name])?;
1137    }
1138    match edit {
1139        NativeGroupEdit::Leave { .. } => {
1140            if tx.execute("UPDATE contact SET is_in_chat_room=2 WHERE username=?1", [room])? != 1 { return Err(ProfileError::InvalidMetadata); }
1141        }
1142        NativeGroupEdit::Notifications { muted, clear_minimized, .. } => {
1143            if tx.execute("UPDATE contact SET chat_room_notify=?2,flag=CASE WHEN ?3 THEN flag & ~268435456 ELSE flag END WHERE username=?1 AND flag IS NOT NULL", rusqlite::params![room,i64::from(!muted),clear_minimized])? != 1 { return Err(ProfileError::InvalidMetadata); }
1144        }
1145        NativeGroupEdit::Unfold { .. } => {
1146            let flag: Option<i64> = tx.query_row("SELECT flag FROM contact WHERE username=?1 AND local_type=2 AND (delete_flag IS NULL OR delete_flag=0) AND is_in_chat_room=1", [room], |r| r.get(0)).optional()?.flatten();
1147            let flag = flag.and_then(|flag| u32::try_from(flag).ok()).filter(|flag| flag & 2 != 0).ok_or(ProfileError::InvalidMetadata)?;
1148            if tx.execute("UPDATE contact SET flag=?2 WHERE username=?1", rusqlite::params![room,flag & !0x10000000])? != 1 { return Err(ProfileError::InvalidMetadata); }
1149        }
1150        NativeGroupEdit::ClearAnnouncement => {
1151            tx.execute("INSERT INTO chat_room_info_detail(room_id_,username_,announcement_,xml_announcement_)VALUES(?1,?2,'','') ON CONFLICT(room_id_) DO UPDATE SET announcement_='',xml_announcement_=''", rusqlite::params![id,room])?;
1152        }
1153        _ => {}
1154    }
1155    tx.commit()?;
1156    Ok(())
1157}
1158struct ProtoChunk<'a> {
1159    field: u64,
1160    wire: u8,
1161    raw: &'a [u8],
1162    value: &'a [u8],
1163}
1164fn proto_chunks(data: &[u8]) -> Result<Vec<ProtoChunk<'_>>> {
1165    if data.len() > 1024 * 1024 {
1166        return Err(ProfileError::ResourceLimit);
1167    }
1168    let mut at = 0;
1169    let mut chunks = vec![];
1170    while at < data.len() {
1171        let start = at;
1172        let tag = proto_var(data, &mut at)?;
1173        let field = tag >> 3;
1174        let wire = (tag & 7) as u8;
1175        if field == 0 || field >= 1 << 29 {
1176            return Err(ProfileError::InvalidMetadata);
1177        }
1178        let mut value_start = at;
1179        match wire {
1180            0 => {
1181                proto_var(data, &mut at)?;
1182            }
1183            1 => at = at.checked_add(8).ok_or(ProfileError::ResourceLimit)?,
1184            5 => at = at.checked_add(4).ok_or(ProfileError::ResourceLimit)?,
1185            2 => {
1186                let size = usize::try_from(proto_var(data, &mut at)?)
1187                    .map_err(|_| ProfileError::ResourceLimit)?;
1188                value_start = at;
1189                at = at.checked_add(size).ok_or(ProfileError::ResourceLimit)?
1190            }
1191            _ => return Err(ProfileError::InvalidMetadata),
1192        }
1193        let raw = data.get(start..at).ok_or(ProfileError::InvalidMetadata)?;
1194        let value = data
1195            .get(value_start..at)
1196            .ok_or(ProfileError::InvalidMetadata)?;
1197        chunks.push(ProtoChunk {
1198            field,
1199            wire,
1200            raw,
1201            value,
1202        });
1203        if chunks.len() > 65536 {
1204            return Err(ProfileError::ResourceLimit);
1205        }
1206    }
1207    Ok(chunks)
1208}
1209fn proto_bytes(field: u64, value: &[u8], out: &mut Vec<u8>) {
1210    var_out((field << 3) | 2, out);
1211    var_out(value.len() as u64, out);
1212    out.extend_from_slice(value)
1213}
1214fn member_name(data: &[u8]) -> Result<String> {
1215    let mut name = None;
1216    let mut display_seen = false;
1217    for chunk in proto_chunks(data)? {
1218        match chunk.field {
1219            1 => {
1220                if chunk.wire != 2 || name.is_some() {
1221                    return Err(ProfileError::InvalidMetadata);
1222                }
1223                let value =
1224                    std::str::from_utf8(chunk.value).map_err(|_| ProfileError::InvalidMetadata)?;
1225                identity(value)?;
1226                name = Some(value.to_owned());
1227            }
1228            2 => {
1229                if chunk.wire != 2 || display_seen {
1230                    return Err(ProfileError::InvalidMetadata);
1231                }
1232                std::str::from_utf8(chunk.value).map_err(|_| ProfileError::InvalidMetadata)?;
1233                display_seen = true;
1234            }
1235            _ => {}
1236        }
1237    }
1238    name.ok_or(ProfileError::InvalidMetadata)
1239}
1240fn patch_member(old: Option<&[u8]>, patch: &NativeMemberPatch) -> Result<Vec<u8>> {
1241    identity(&patch.username)?;
1242    if patch.opaque.as_ref().is_some_and(|b| b.len() > 65536) {
1243        return Err(ProfileError::ResourceLimit);
1244    }
1245    if patch
1246        .display
1247        .as_ref()
1248        .is_some_and(|s| s.len() > 65536 || s.contains('\0'))
1249    {
1250        return Err(ProfileError::InvalidMetadata);
1251    }
1252    let mut out = vec![];
1253    let mut wrote_display = false;
1254    let mut wrote_flags = false;
1255    let mut wrote_opaque = false;
1256    if let Some(old) = old {
1257        if member_name(old)? != patch.username {
1258            return Err(ProfileError::InvalidMetadata);
1259        }
1260        for chunk in proto_chunks(old)? {
1261            if chunk.field == 2 {
1262                if let Some(display) = &patch.display {
1263                    proto_bytes(2, display.as_bytes(), &mut out);
1264                    wrote_display = true;
1265                } else {
1266                    out.extend_from_slice(chunk.raw);
1267                }
1268            } else if chunk.field == 3 && patch.flags.is_some() {
1269                if chunk.wire != 0 || wrote_flags {
1270                    return Err(ProfileError::InvalidMetadata);
1271                }
1272                var_out(3 << 3, &mut out);
1273                var_out(u64::from(patch.flags.unwrap()), &mut out);
1274                wrote_flags = true;
1275            } else if chunk.field == 4 && patch.opaque.is_some() {
1276                if chunk.wire != 2 || wrote_opaque {
1277                    return Err(ProfileError::InvalidMetadata);
1278                }
1279                proto_bytes(4, patch.opaque.as_deref().unwrap(), &mut out);
1280                wrote_opaque = true;
1281            } else {
1282                out.extend_from_slice(chunk.raw);
1283            }
1284        }
1285    } else {
1286        proto_bytes(1, patch.username.as_bytes(), &mut out);
1287    }
1288    if !wrote_display {
1289        if let Some(display) = &patch.display {
1290            proto_bytes(2, display.as_bytes(), &mut out)
1291        }
1292    }
1293    if !wrote_flags {
1294        if let Some(flags) = patch.flags {
1295            var_out(3 << 3, &mut out);
1296            var_out(u64::from(flags), &mut out);
1297        }
1298    }
1299    if !wrote_opaque {
1300        if let Some(opaque) = &patch.opaque {
1301            proto_bytes(4, opaque, &mut out);
1302        }
1303    }
1304    Ok(out)
1305}
1306/// Translate only the proved network member identity/display semantics into the
1307/// local repeated-field1 representation. Caller proves roster completeness (for
1308/// example, a separate complete member identity list in the server reply). Unknown
1309/// root/member protobuf chunks, existing owner and unrelated contact fields are
1310/// retained byte-for-byte. No network version is guessed into opaque fields.
1311pub fn apply_native_roster_patch(
1312    location: &ProfileLocation,
1313    keys: &KeyFile,
1314    input: &NativeRosterPatch,
1315    options: &WriteOptions,
1316) -> Result<()> {
1317    ownership(location, keys, options)?;
1318    identity(&input.username)?;
1319    if !input.username.ends_with("@chatroom") || input.members.len() > 10000 {
1320        return Err(ProfileError::InvalidMetadata);
1321    }
1322    let mut patches = std::collections::BTreeMap::new();
1323    if let Some(ids) = &input.administrator_ids {
1324        let mut seen = std::collections::BTreeSet::new();
1325        for id in ids {
1326            identity(id)?;
1327            if !seen.insert(id) || seen.len() > 10000 {
1328                return Err(ProfileError::InvalidMetadata);
1329            }
1330        }
1331    }
1332    for member in &input.members {
1333        identity(&member.username)?;
1334        if patches.insert(member.username.clone(), member).is_some() {
1335            return Err(ProfileError::InvalidMetadata);
1336        }
1337    }
1338    let db = find_database(location, "contact.db")?;
1339    let c = open_write(location, &db, keys, options.allow_plaintext)?;
1340    check_known_table(&c, "chat_room")?;
1341    check_known_table(&c, "chatroom_member")?;
1342    let room = c
1343        .query_row(
1344            "SELECT id,ext_buffer FROM chat_room WHERE username=?1",
1345            [&input.username],
1346            |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Option<Vec<u8>>>(1)?)),
1347        )
1348        .optional()?;
1349    let old = room
1350        .as_ref()
1351        .and_then(|(_, bytes)| bytes.as_deref())
1352        .unwrap_or(&[]);
1353    let chunks = proto_chunks(old)?;
1354    let mut old_members = std::collections::BTreeMap::new();
1355    for chunk in &chunks {
1356        if chunk.field == 1 {
1357            if chunk.wire != 2 {
1358                return Err(ProfileError::InvalidMetadata);
1359            }
1360            let name = member_name(chunk.value)?;
1361            if old_members.insert(name, chunk.value).is_some() {
1362                return Err(ProfileError::InvalidMetadata);
1363            }
1364        }
1365    }
1366    let mut result_members = if input.complete_roster {
1367        std::collections::BTreeSet::new()
1368    } else {
1369        old_members.keys().cloned().collect()
1370    };
1371    if !input.complete_roster {
1372        if let Some((id, _)) = &room {
1373            let names=c.prepare("SELECT c.username FROM chatroom_member m JOIN contact c ON c.id=m.member_id WHERE m.room_id=?1")?.query_map([id],|r|r.get::<_,String>(0))?.collect::<std::result::Result<Vec<_>,_>>()?;
1374            result_members.extend(names);
1375        }
1376    }
1377    result_members.extend(patches.keys().cloned());
1378    let mut packed = vec![];
1379    let mut written = std::collections::BTreeSet::new();
1380    for chunk in chunks {
1381        if chunk.field == 5 && input.administrator_ids.is_some() {
1382            if chunk.wire != 2 {
1383                return Err(ProfileError::InvalidMetadata);
1384            }
1385            continue;
1386        }
1387        if chunk.field != 1 {
1388            packed.extend_from_slice(chunk.raw);
1389            continue;
1390        }
1391        let name = member_name(chunk.value)?;
1392        if !result_members.contains(&name) {
1393            continue;
1394        }
1395        if let Some(patch) = patches.get(&name) {
1396            proto_bytes(1, &patch_member(Some(chunk.value), patch)?, &mut packed)
1397        } else {
1398            packed.extend_from_slice(chunk.raw)
1399        }
1400        written.insert(name);
1401    }
1402    // New members have no invented opaque metadata. Their contact rows must already
1403    // exist; the SQL projection checks that binding and allocates destination IDs.
1404    for name in result_members.difference(&written) {
1405        let empty = NativeMemberPatch {
1406            flags: None,
1407            opaque: None,
1408            username: name.clone(),
1409            display: None,
1410        };
1411        let patch = patches.get(name).copied().unwrap_or(&empty);
1412        proto_bytes(1, &patch_member(None, patch)?, &mut packed);
1413    }
1414    let mut fields = RawFields::new();
1415    if let Some(ids) = &input.administrator_ids {
1416        for id in ids {
1417            proto_bytes(5, id.as_bytes(), &mut packed);
1418        }
1419    }
1420    fields.insert("ext_buffer".into(), SqlValue::Blob(STANDARD.encode(packed)));
1421    drop(c);
1422    apply_group_patch(
1423        location,
1424        keys,
1425        &GroupProjection {
1426            username: input.username.clone(),
1427            fields,
1428            detail_fields: None,
1429            members: result_members.into_iter().collect(),
1430            complete_roster: input.complete_roster,
1431        },
1432        options,
1433    )
1434}
1435
1436/// WCDB compression state 4 observed on the supported fields. Compression is
1437/// independent of media type; raw protocol bytes stay in the durable sidecar.
1438pub fn compressed_profile_field(bytes: &[u8]) -> Result<SqlValue> {
1439    if bytes.len() > 4 * 1024 * 1024 {
1440        return Err(ProfileError::ResourceLimit);
1441    }
1442    let compressed =
1443        zstd::stream::encode_all(bytes, 3).map_err(|_| ProfileError::InvalidMetadata)?;
1444    Ok(SqlValue::Blob(STANDARD.encode(compressed)))
1445}
1446/// Find one exact server identity in the sole supported ordinary shard. This
1447/// never adopts a first match from an ambiguous or rotated shard family.
1448pub fn find_projected_message(
1449    location: &ProfileLocation,
1450    keys: &KeyFile,
1451    peer: &str,
1452    server_id: &str,
1453    options: &WriteOptions,
1454) -> Result<Option<SourceMessageId>> {
1455    ownership(location, keys, options)?;
1456    identity(peer)?;
1457    crate::writer::require_single_message_shard(location)?;
1458    let sid = server_id
1459        .parse::<u64>()
1460        .map_err(|_| ProfileError::InvalidMetadata)?;
1461    if sid == 0 {
1462        return Err(ProfileError::InvalidMetadata);
1463    }
1464    let db = find_database(location, "message_0.db")?;
1465    let c = open_write(location, &db, keys, options.allow_plaintext)?;
1466    let table = format!("Msg_{:x}", Md5::digest(peer.as_bytes()));
1467    let present = c.query_row(
1468        "SELECT count(*)FROM sqlite_master WHERE type='table' AND name=?1",
1469        [&table],
1470        |r| r.get::<_, i64>(0),
1471    )?;
1472    if present == 0 {
1473        return Ok(None);
1474    }
1475    crate::writer::exact_columns(&c, &db, &table, crate::writer::MESSAGE_COLUMNS)?;
1476    let ids = c
1477        .prepare(&format!(
1478            "SELECT local_id FROM {} WHERE server_id=?1 LIMIT 2",
1479            quote_identifier(&table)
1480        ))?
1481        .query_map([sid as i64], |r| r.get::<_, i64>(0))?
1482        .collect::<std::result::Result<Vec<_>, _>>()?;
1483    if ids.len() > 1 {
1484        return Err(ProfileError::InvalidMetadata);
1485    }
1486    Ok(ids.first().map(|id| SourceMessageId {
1487        database: db,
1488        table,
1489        local_id: id.to_string(),
1490    }))
1491}
1492/// Choose the first unused millisecond slot in this message's second, or retain
1493/// the already stored slot for a replay. This is an independent collision policy
1494/// within the observed native 1000-slots/second representation, not a recovered
1495/// native rollover algorithm. Caller holds the lease through insertion.
1496pub fn choose_message_sort_sequence(
1497    location: &ProfileLocation,
1498    keys: &KeyFile,
1499    peer: &str,
1500    server_id: &str,
1501    time: i64,
1502    options: &WriteOptions,
1503) -> Result<i64> {
1504    ownership(location, keys, options)?;
1505    identity(peer)?;
1506    if time < 0 {
1507        return Err(ProfileError::InvalidMetadata);
1508    }
1509    let base = time.checked_mul(1000).ok_or(ProfileError::ResourceLimit)?;
1510    let end = base.checked_add(999).ok_or(ProfileError::ResourceLimit)?;
1511    let db = find_database(location, "message_0.db")?;
1512    let c = open_write(location, &db, keys, options.allow_plaintext)?;
1513    let table = format!("Msg_{:x}", Md5::digest(peer.as_bytes()));
1514    if c.query_row(
1515        "SELECT count(*)FROM sqlite_master WHERE type='table' AND name=?1",
1516        [&table],
1517        |r| r.get::<_, i64>(0),
1518    )? == 0
1519    {
1520        return Ok(base);
1521    }
1522    if let Some(id) = find_projected_message(location, keys, peer, server_id, options)? {
1523        let value = c.query_row(
1524            &format!(
1525                "SELECT create_time,sort_seq FROM {} WHERE local_id=?1",
1526                quote_identifier(&table)
1527            ),
1528            [id.local_id
1529                .parse::<i64>()
1530                .map_err(|_| ProfileError::InvalidMetadata)?],
1531            |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
1532        )?;
1533        if value.0 != time {
1534            return Err(ProfileError::InvalidMetadata);
1535        }
1536        return Ok(value.1);
1537    }
1538    let used = c
1539        .prepare(&format!(
1540            "SELECT sort_seq FROM {} WHERE sort_seq BETWEEN ?1 AND ?2",
1541            quote_identifier(&table)
1542        ))?
1543        .query_map(params![base, end], |r| r.get::<_, i64>(0))?
1544        .collect::<std::result::Result<std::collections::BTreeSet<_>, _>>()?;
1545    (base..=end)
1546        .find(|n| !used.contains(n))
1547        .ok_or(ProfileError::ResourceLimit)
1548}
1549#[derive(Clone, Debug, Serialize, Deserialize)]
1550#[serde(rename_all = "camelCase", deny_unknown_fields)]
1551pub struct MessageSessionProjection {
1552    pub source: SourceMessageId,
1553    pub peer: String,
1554    pub summary: String,
1555    pub outgoing: bool,
1556    pub unread: bool,
1557    pub sender: Option<String>,
1558    pub sender_display: Option<String>,
1559    /// Only a durable coordinator intent can establish that an existing row came
1560    /// from this unfinished projection. Imported/adopted rows never gain unread.
1561    pub created_by_this_intent: bool,
1562}
1563/// Reconcile a committed message into a desktop session, preserving drafts,
1564/// unrelated unread rows and preexisting counter/list discrepancies. It makes no
1565/// server/phone read request and is retryable after a message-only commit.
1566pub fn apply_message_session_projection(
1567    location: &ProfileLocation,
1568    keys: &KeyFile,
1569    input: &MessageSessionProjection,
1570    options: &WriteOptions,
1571) -> Result<()> {
1572    ownership(location, keys, options)?;
1573    identity(&input.peer)?;
1574    if input.summary.len() > 4 * 1024 * 1024
1575        || input.source.table != format!("Msg_{:x}", Md5::digest(input.peer.as_bytes()))
1576    {
1577        return Err(ProfileError::InvalidMetadata);
1578    }
1579    let messages = open_write(
1580        location,
1581        &input.source.database,
1582        keys,
1583        options.allow_plaintext,
1584    )?;
1585    crate::writer::exact_columns(
1586        &messages,
1587        &input.source.database,
1588        &input.source.table,
1589        crate::writer::MESSAGE_COLUMNS,
1590    )?;
1591    let local = input
1592        .source
1593        .local_id
1594        .parse::<i64>()
1595        .map_err(|_| ProfileError::InvalidMetadata)?;
1596    let (server, time, kind) = messages.query_row(
1597        &format!(
1598            "SELECT server_id,create_time,local_type FROM {} WHERE local_id=?1",
1599            quote_identifier(&input.source.table)
1600        ),
1601        [local],
1602        |r| {
1603            Ok((
1604                r.get::<_, i64>(0)?,
1605                r.get::<_, i64>(1)?,
1606                r.get::<_, i64>(2)?,
1607            ))
1608        },
1609    )?;
1610    drop(messages);
1611    let db = find_database(location, "session.db")?;
1612    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
1613    for t in [
1614        "SessionTable",
1615        "SessionUnreadListTable_1",
1616        "SessionUnreadStatTable_1",
1617    ] {
1618        check_known_table(&c, t)?;
1619    }
1620    let tx = c.transaction()?;
1621    let session = map_id(&tx, "Name2Id", "user_name", &input.peer)?;
1622    let old=tx.query_row("SELECT unread_count,last_msg_locald_id,last_timestamp,last_clear_unread_timestamp FROM SessionTable WHERE username=?1",[&input.peer],|r|Ok((r.get::<_,Option<i64>>(0)?.unwrap_or(0),r.get::<_,Option<i64>>(1)?.unwrap_or(0),r.get::<_,Option<i64>>(2)?.unwrap_or(0),r.get::<_,Option<i64>>(3)?.unwrap_or(0)))).optional()?;
1623    if old.is_none() {
1624        tx.execute("INSERT INTO SessionTable(username,type,unread_count,unread_first_msg_srv_id,unread_first_pat_msg_local_id,unread_first_pat_msg_sort_seq,is_hidden,summary,draft,status,last_timestamp,sort_timestamp,last_clear_unread_timestamp,last_msg_locald_id,last_msg_type,last_msg_sub_type,last_msg_sender,last_sender_display_name,last_msg_ext_type)VALUES(?1,0,0,0,0,0,0,'','',0,0,0,0,0,0,0,'','',0)",[&input.peer])?;
1625    }
1626    let old = old.unwrap_or((0, 0, 0, 0));
1627    if old.0 < 0 {
1628        return Err(ProfileError::InvalidMetadata);
1629    }
1630    let unread=tx.query_row("SELECT local_id,create_time FROM SessionUnreadListTable_1 WHERE username_id=?1 AND server_id=?2",params![session,server],|r|Ok((r.get::<_,i64>(0)?,r.get::<_,i64>(1)?))).optional()?;
1631    if unread.is_some_and(|v| v != (local, time)) {
1632        return Err(ProfileError::InvalidMetadata);
1633    }
1634    let done = old.1 == local && old.2 == time;
1635    if !input.outgoing
1636        && input.unread
1637        && input.created_by_this_intent
1638        && unread.is_none()
1639        && !done
1640        && old.3 < time
1641    {
1642        // A newer sidebar summary is not a read marker: sync may arrive out of
1643        // order. Only an intent-owned new row and the explicit local clear cutoff
1644        // permit this addition; imported/adopted rows never regain unread.
1645        tx.execute("INSERT INTO SessionUnreadListTable_1(username_id,server_id,local_id,create_time)VALUES(?1,?2,?3,?4)",params![session,server,local,time])?;
1646        tx.execute("UPDATE SessionTable SET unread_first_msg_srv_id=CASE WHEN unread_count=0 THEN ?1 ELSE unread_first_msg_srv_id END,unread_count=unread_count+1 WHERE username=?2",params![server,input.peer])?;
1647    }
1648    if old.2 <= time && (old.2 < time || old.1 <= local) {
1649        let sender = if input.peer.ends_with("@chatroom") {
1650            input.sender.as_deref().unwrap_or("")
1651        } else {
1652            ""
1653        };
1654        let display = if input.peer.ends_with("@chatroom") {
1655            input.sender_display.as_deref().unwrap_or("")
1656        } else {
1657            ""
1658        };
1659        tx.execute("UPDATE SessionTable SET summary=?1,last_timestamp=?2,sort_timestamp=?2,last_msg_locald_id=?3,last_msg_type=?4,last_msg_sub_type=?5,last_msg_sender=?6,last_sender_display_name=?7,last_msg_ext_type=0 WHERE username=?8",params![input.summary,time,local,kind&0x7fffffff,kind>>32,sender,display,input.peer])?;
1660    }
1661    tx.commit()?;
1662    Ok(())
1663}
1664
1665#[cfg(test)]
1666mod pin_tests {
1667    use super::*;
1668
1669    fn fixture(encrypted: bool) -> (tempfile::TempDir, ProfileLocation, KeyFile, WriteOptions) {
1670        let temp = tempfile::tempdir().unwrap();
1671        let root = temp.path().join("account");
1672        let (location, keys) = if encrypted {
1673            let (location, keys, _) = initialize_profile(&root, &[42; 32]).unwrap();
1674            (location, keys)
1675        } else {
1676            for (relative, sql) in [
1677                ("contact/contact.db", include_str!("layout/contact.db.sql")),
1678                ("session/session.db", include_str!("layout/session.db.sql")),
1679            ] {
1680                let path = root.join("db_storage").join(relative);
1681                std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1682                Connection::open(path).unwrap().execute_batch(sql).unwrap();
1683            }
1684            (
1685                ProfileLocation::new(&root).unwrap(),
1686                KeyFile::empty("account"),
1687            )
1688        };
1689        let options = WriteOptions {
1690            exclusive_ownership_confirmed: true,
1691            allow_plaintext: !encrypted,
1692            durable_intent_id: "synthetic-pin-receipt".into(),
1693        };
1694        (temp, location, keys, options)
1695    }
1696
1697    #[test]
1698    fn sparse_new_contacts_initialize_zero_without_resetting_existing_deletion_state() {
1699        for encrypted in [false, true] {
1700            let (_temp, location, keys, options) = fixture(encrypted);
1701            let contact = open_write(&location, "contact/contact.db", &keys, !encrypted).unwrap();
1702            let patch = |name: &str, fields: RawFields| {
1703                apply_contact_patch(
1704                    &location,
1705                    &keys,
1706                    &ContactProjection {
1707                        username: name.into(),
1708                        fields,
1709                    },
1710                    &options,
1711                )
1712                .unwrap();
1713            };
1714            patch(
1715                "new_peer",
1716                [("flag".into(), SqlValue::Integer("1".into()))]
1717                    .into_iter()
1718                    .collect(),
1719            );
1720            let deletion = |name: &str| {
1721                contact
1722                    .query_row(
1723                        "SELECT delete_flag FROM contact WHERE username=?1",
1724                        [name],
1725                        |row| row.get::<_, Option<i64>>(0),
1726                    )
1727                    .unwrap()
1728            };
1729            assert_eq!(deletion("new_peer"), Some(0));
1730            for (name, value) in [
1731                ("legacy_peer", SqlValue::Null),
1732                ("deleted_peer", SqlValue::Integer("1".into())),
1733            ] {
1734                patch(name, [("delete_flag".into(), value)].into_iter().collect());
1735                patch(
1736                    name,
1737                    [("nick_name".into(), SqlValue::Text("Updated name".into()))]
1738                        .into_iter()
1739                        .collect(),
1740                );
1741                assert_eq!(
1742                    deletion(name),
1743                    if name == "legacy_peer" { None } else { Some(1) }
1744                );
1745            }
1746        }
1747    }
1748    #[test]
1749    fn null_delete_flags_allow_mute_projection_without_normalizing_the_contact_row() {
1750        for encrypted in [false, true] {
1751            let (_temp, location, keys, options) = fixture(encrypted);
1752            let contact = open_write(&location, "contact/contact.db", &keys, !encrypted).unwrap();
1753            contact.execute_batch("INSERT INTO contact(username,local_type,flag,delete_flag) VALUES('legacy',1,2049,NULL),('deleted',1,2049,1),('malformed',1,2049,'bad');").unwrap();
1754            assert!(
1755                apply_contact_mute_patch(&location, &keys, "legacy", true, &options)
1756                    .unwrap()
1757                    .unwrap()
1758                    .changed
1759            );
1760            let saved = contact
1761                .query_row(
1762                    "SELECT flag,delete_flag FROM contact WHERE username='legacy'",
1763                    [],
1764                    |row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?)),
1765                )
1766                .unwrap();
1767            assert_eq!(saved, (2561, None));
1768            for name in ["deleted", "malformed"] {
1769                assert!(
1770                    apply_contact_mute_patch(&location, &keys, name, true, &options)
1771                        .unwrap()
1772                        .is_none()
1773                );
1774            }
1775        }
1776    }
1777    #[test]
1778    fn pin_projection_preserves_other_state_and_replays_without_rewinding_activity() {
1779        for encrypted in [false, true] {
1780            let (_temp, location, keys, options) = fixture(encrypted);
1781            let contact = open_write(&location, "contact/contact.db", &keys, !encrypted).unwrap();
1782            let session = open_write(&location, "session/session.db", &keys, !encrypted).unwrap();
1783            contact.execute_batch("INSERT INTO contact(id,username,flag,local_type,delete_flag,is_in_chat_room,chat_room_notify)VALUES(1,'peer',268436099,1,NULL,0,0),(2,'room@chatroom',268435586,2,NULL,1,0);").unwrap();
1784            session.execute_batch("INSERT INTO SessionTable(username,unread_count,draft,status,is_hidden,sort_timestamp,last_timestamp,last_msg_locald_id)VALUES('peer',4,'Keep draft',7,0,100,90,12),('room@chatroom',2,'Room draft',3,0,100,99,15);").unwrap();
1785            let saved_flag = || {
1786                contact
1787                    .query_row(
1788                        "SELECT flag FROM contact WHERE username='peer'",
1789                        [],
1790                        |row| row.get::<_, i64>(0),
1791                    )
1792                    .unwrap()
1793            };
1794            let saved_sort = || {
1795                session
1796                    .query_row(
1797                        "SELECT sort_timestamp FROM SessionTable WHERE username='peer'",
1798                        [],
1799                        |row| row.get::<_, i64>(0),
1800                    )
1801                    .unwrap()
1802            };
1803            let baseline = saved_flag();
1804            let result =
1805                apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &options)
1806                    .unwrap()
1807                    .unwrap();
1808            assert!(result.changed && result.session_projected);
1809            assert!(!result.cross_database_atomic);
1810            assert_eq!(saved_flag(), baseline | 0x800);
1811            assert_eq!(saved_sort(), 200);
1812            assert!(
1813                !apply_contact_pin_patch(&location, &keys, "peer", true, false, 300, &options)
1814                    .unwrap()
1815                    .unwrap()
1816                    .changed
1817            );
1818            assert_eq!(saved_sort(), 200);
1819            session
1820                .execute(
1821                    "UPDATE SessionTable SET sort_timestamp=400 WHERE username='peer'",
1822                    [],
1823                )
1824                .unwrap();
1825            apply_contact_pin_patch(&location, &keys, "peer", false, false, 250, &options)
1826                .unwrap()
1827                .unwrap();
1828            assert_eq!(saved_flag(), baseline);
1829            assert_eq!(saved_sort(), 400);
1830            // Model an interrupted session-first commit before its contact flag.
1831            session
1832                .execute(
1833                    "UPDATE SessionTable SET sort_timestamp=500 WHERE username='peer'",
1834                    [],
1835                )
1836                .unwrap();
1837            apply_contact_pin_patch(&location, &keys, "peer", true, true, 450, &options)
1838                .unwrap()
1839                .unwrap();
1840            assert_eq!(saved_flag(), (baseline & !0x10000080) | 0x800);
1841            assert_eq!(saved_sort(), 500);
1842            let unrelated = session.query_row("SELECT unread_count,draft,status,is_hidden,last_timestamp,last_msg_locald_id FROM SessionTable WHERE username='peer'", [], |row| Ok((row.get::<_,i64>(0)?,row.get::<_,String>(1)?,row.get::<_,i64>(2)?,row.get::<_,i64>(3)?,row.get::<_,i64>(4)?,row.get::<_,i64>(5)?))).unwrap();
1843            assert_eq!(unrelated, (4, "Keep draft".into(), 7, 0, 90, 12));
1844            apply_contact_pin_patch(&location, &keys, "room@chatroom", true, true, 600, &options)
1845                .unwrap()
1846                .unwrap();
1847            assert_eq!(
1848                contact
1849                    .query_row(
1850                        "SELECT flag,chat_room_notify FROM contact WHERE username='room@chatroom'",
1851                        [],
1852                        |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1853                    )
1854                    .unwrap(),
1855                (2050, 0)
1856            );
1857            apply_contact_pin_patch(
1858                &location,
1859                &keys,
1860                "room@chatroom",
1861                false,
1862                false,
1863                700,
1864                &options,
1865            )
1866            .unwrap()
1867            .unwrap();
1868            assert_eq!(
1869                session
1870                    .query_row(
1871                        "SELECT sort_timestamp FROM SessionTable WHERE username='room@chatroom'",
1872                        [],
1873                        |row| row.get::<_, i64>(0)
1874                    )
1875                    .unwrap(),
1876                700
1877            );
1878        }
1879    }
1880
1881    #[test]
1882    fn pin_projection_preflights_both_schemas_and_keeps_wal_modes() {
1883        let (_temp, location, keys, options) = fixture(false);
1884        let contact = open_write(&location, "contact/contact.db", &keys, true).unwrap();
1885        let session = open_write(&location, "session/session.db", &keys, true).unwrap();
1886        contact.execute_batch("PRAGMA journal_mode=WAL;INSERT INTO contact(id,username,flag,local_type,delete_flag,is_in_chat_room)VALUES(1,'peer',1,1,0,0),(2,'no_session',1,1,0,0),(3,'missing_flag',NULL,1,0,0),(4,'filehelper',1,1,0,0),(5,'left@chatroom',2,2,0,2);").unwrap();
1887        session.execute_batch("PRAGMA journal_mode=WAL;INSERT INTO SessionTable(username,sort_timestamp)VALUES('peer',100),('missing_flag',100),('filehelper',100),('left@chatroom',100);").unwrap();
1888        for name in [
1889            "missing",
1890            "no_session",
1891            "missing_flag",
1892            "filehelper",
1893            "left@chatroom",
1894        ] {
1895            assert!(
1896                apply_contact_pin_patch(&location, &keys, name, true, false, 200, &options)
1897                    .unwrap()
1898                    .is_none(),
1899                "{name}"
1900            );
1901        }
1902        session
1903            .execute_batch("ALTER TABLE SessionTable ADD COLUMN unknown_column TEXT;")
1904            .unwrap();
1905        assert!(matches!(
1906            apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &options),
1907            Err(ProfileError::UnsupportedSchema(_))
1908        ));
1909        assert_eq!(
1910            contact
1911                .query_row(
1912                    "SELECT flag FROM contact WHERE username='peer'",
1913                    [],
1914                    |row| row.get::<_, i64>(0)
1915                )
1916                .unwrap(),
1917            1
1918        );
1919        assert_eq!(
1920            session
1921                .query_row(
1922                    "SELECT sort_timestamp FROM SessionTable WHERE username='peer'",
1923                    [],
1924                    |row| row.get::<_, i64>(0)
1925                )
1926                .unwrap(),
1927            100
1928        );
1929        session
1930            .execute_batch("ALTER TABLE SessionTable DROP COLUMN unknown_column;")
1931            .unwrap();
1932        let mut unowned = options.clone();
1933        unowned.exclusive_ownership_confirmed = false;
1934        assert!(matches!(
1935            apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &unowned),
1936            Err(ProfileError::SnapshotRequired)
1937        ));
1938        assert!(
1939            apply_contact_pin_patch(&location, &keys, "peer", false, true, 200, &options).is_err()
1940        );
1941        apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &options)
1942            .unwrap()
1943            .unwrap();
1944        for db in [&contact, &session] {
1945            assert_eq!(
1946                db.query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))
1947                    .unwrap(),
1948                "wal"
1949            );
1950        }
1951    }
1952
1953    #[test]
1954    fn group_unfold_preserves_notification_state_other_members_and_opaque_fields() {
1955        let (_temp, location, keys, options) = fixture(false);
1956        let contact = open_write(&location, "contact/contact.db", &keys, true).unwrap();
1957        contact.execute_batch("INSERT INTO contact(id,username,flag,local_type,delete_flag,is_in_chat_room,chat_room_notify)VALUES(1,'room@chatroom',268435458,2,NULL,1,0);INSERT INTO chat_room(id,username)VALUES(1,'room@chatroom');").unwrap();
1958        let before = [
1959            10, 14, 10, 4, b's', b'e', b'l', b'f', 24, 129, 64, 34, 3, 1, 2, 3, 10, 9, 10, 4, b'p',
1960            b'e', b'e', b'r', 24, 129, 64, 42, 3, 9, 8, 7,
1961        ];
1962        let expected = [
1963            10, 13, 10, 4, b's', b'e', b'l', b'f', 24, 1, 34, 3, 1, 2, 3, 10, 9, 10, 4, b'p', b'e',
1964            b'e', b'r', 24, 129, 64, 42, 3, 9, 8, 7,
1965        ];
1966        contact
1967            .execute("UPDATE chat_room SET ext_buffer=?1", [before.as_slice()])
1968            .unwrap();
1969        for _ in 0..2 {
1970            apply_native_group_edit(
1971                &location,
1972                &keys,
1973                "room@chatroom",
1974                NativeGroupEdit::Unfold { own: "self".into() },
1975                &options,
1976            )
1977            .unwrap();
1978        }
1979        assert_eq!(
1980            contact
1981                .query_row(
1982                    "SELECT flag,chat_room_notify FROM contact WHERE username='room@chatroom'",
1983                    [],
1984                    |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1985                )
1986                .unwrap(),
1987            (2, 0)
1988        );
1989        assert_eq!(
1990            contact
1991                .query_row("SELECT ext_buffer FROM chat_room", [], |row| row
1992                    .get::<_, Vec<u8>>(0))
1993                .unwrap(),
1994            expected
1995        );
1996        // Invalid contact metadata rolls back even the already prepared packed edit.
1997        contact
1998            .execute("UPDATE contact SET flag=4294967296", [])
1999            .unwrap();
2000        contact
2001            .execute("UPDATE chat_room SET ext_buffer=?1", [before.as_slice()])
2002            .unwrap();
2003        assert!(apply_native_group_edit(
2004            &location,
2005            &keys,
2006            "room@chatroom",
2007            NativeGroupEdit::Unfold { own: "self".into() },
2008            &options
2009        )
2010        .is_err());
2011        assert_eq!(
2012            contact
2013                .query_row("SELECT ext_buffer FROM chat_room", [], |row| row
2014                    .get::<_, Vec<u8>>(0))
2015                .unwrap(),
2016            before
2017        );
2018    }
2019}