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=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::num(&fields, "delete_flag") != Some(0) {
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        let schema = crate::schema::inspect(&tx, &db)?;
774        let expected = schema
775            .tables
776            .iter()
777            .find(|t| t.name == table)
778            .ok_or(ProfileError::InvalidMetadata)?;
779        if fields.len() + 1 != expected.columns.len()
780            || expected
781                .columns
782                .iter()
783                .any(|col| col.name != "local_id" && !fields.contains_key(&col.name))
784        {
785            return Err(ProfileError::InvalidMetadata);
786        }
787        // No invented tie-breaker. The explicit adapter must provide a collision-free
788        // sort sequence, or resolve the collision under a separately proved policy.
789        let sort = match fields.get("sort_seq") {
790            Some(SqlValue::Integer(n)) => n
791                .parse::<i64>()
792                .map_err(|_| ProfileError::InvalidMetadata)?,
793            _ => return Err(ProfileError::InvalidMetadata),
794        };
795        let count: i64 = tx.query_row(
796            &format!(
797                "SELECT count(*) FROM {} WHERE sort_seq=?1",
798                quote_identifier(&table)
799            ),
800            [sort],
801            |r| r.get(0),
802        )?;
803        if count != 0 {
804            return Err(ProfileError::WritesUnsupported);
805        }
806        // Temporarily use the next AUTOINCREMENT through an explicit NULL key; SQLite
807        // itself performs allocation and preserves its sqlite_sequence namespace.
808        let names = fields.keys().cloned().collect::<Vec<_>>();
809        let mut vals = vec![];
810        let mut expr = vec![];
811        for v in fields.values() {
812            let (value, text) = bound(v)?;
813            vals.push(value);
814            let p = format!("?{}", vals.len());
815            expr.push(if text {
816                format!("CAST({p} AS TEXT)")
817            } else {
818                p
819            })
820        }
821        tx.execute(
822            &format!(
823                "INSERT INTO {}({})VALUES({})",
824                quote_identifier(&table),
825                names
826                    .iter()
827                    .map(|s| quote_identifier(s))
828                    .collect::<Vec<_>>()
829                    .join(","),
830                expr.join(",")
831            ),
832            rusqlite::params_from_iter(vals),
833        )?;
834        (tx.last_insert_rowid(), true)
835    };
836    tx.commit()?;
837    Ok(AppliedMessage {
838        source_id: SourceMessageId {
839            database: db,
840            table,
841            local_id: local.to_string(),
842        },
843        inserted,
844    })
845}
846/// Explicit session fields. New sessions require a complete row; patches retain
847/// unrelated drafts/read metadata. Caller determines actual native values.
848pub fn apply_session_projection(
849    location: &ProfileLocation,
850    keys: &KeyFile,
851    peer: &str,
852    fields: &RawFields,
853    options: &WriteOptions,
854) -> Result<bool> {
855    ownership(location, keys, options)?;
856    identity(peer)?;
857    let db = find_database(location, "session.db")?;
858    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
859    let tx = c.transaction()?;
860    map_id(&tx, "Name2Id", "user_name", peer)?;
861    let changed = apply_row(
862        &tx,
863        "SessionTable",
864        "username",
865        SqlValue::Text(peer.into()),
866        fields,
867        true,
868    )?;
869    tx.commit()?;
870    Ok(changed)
871}
872
873/// Sparse contact update. Existing rows preserve every omitted field. For a new
874/// row only, omitted nullable/no-default schema columns become SQL NULL as an
875/// explicit independent-client policy, not a claim about native enum defaults.
876pub fn apply_contact_patch(
877    location: &ProfileLocation,
878    keys: &KeyFile,
879    input: &ContactProjection,
880    options: &WriteOptions,
881) -> Result<AppliedContact> {
882    ownership(location, keys, options)?;
883    identity(&input.username)?;
884    let db = find_database(location, "contact.db")?;
885    let c = open_write(location, &db, keys, options.allow_plaintext)?;
886    check_known_table(&c, "contact")?;
887    let existing = c
888        .query_row(
889            "SELECT id FROM contact WHERE username=?1",
890            [&input.username],
891            |r| r.get::<_, i64>(0),
892        )
893        .optional()?;
894    if let Some(id) = existing.filter(|_| input.fields.is_empty()) {
895        return Ok(AppliedContact {
896            local_id: id,
897            changed: false,
898        });
899    }
900    let mut input = input.clone();
901    if existing.is_none() {
902        let mut fields = nullable_fields(&c, &db, "contact", &["id", "username"])?;
903        fields.extend(input.fields);
904        input.fields = fields;
905    }
906    drop(c);
907    apply_contact_projection(location, keys, &input, options)
908}
909fn nullable_fields(
910    c: &Connection,
911    database: &str,
912    table: &str,
913    exclude: &[&str],
914) -> Result<RawFields> {
915    let report = crate::schema::inspect(c, database)?;
916    let t = report
917        .tables
918        .iter()
919        .find(|t| t.name == table)
920        .ok_or(ProfileError::InvalidMetadata)?;
921    let mut fields = RawFields::new();
922    for col in &t.columns {
923        if exclude.contains(&col.name.as_str()) {
924            continue;
925        }
926        if col.not_null || col.default_sql.is_some() {
927            return Err(ProfileError::WritesUnsupported);
928        }
929        fields.insert(col.name.clone(), SqlValue::Null);
930    }
931    Ok(fields)
932}
933/// Sparse group updates use the same explicit SQL NULL policy for a new row.
934/// Existing owner/opaque buffers are preserved unless specifically supplied.
935pub fn apply_group_patch(
936    location: &ProfileLocation,
937    keys: &KeyFile,
938    input: &GroupProjection,
939    options: &WriteOptions,
940) -> Result<()> {
941    ownership(location, keys, options)?;
942    let db = find_database(location, "contact.db")?;
943    let c = open_write(location, &db, keys, options.allow_plaintext)?;
944    check_known_table(&c, "chat_room")?;
945    let exists = c
946        .query_row(
947            "SELECT id FROM chat_room WHERE username=?1",
948            [&input.username],
949            |r| r.get::<_, i64>(0),
950        )
951        .optional()?
952        .is_some();
953    let mut input = input.clone();
954    if !exists {
955        let mut fields = nullable_fields(&c, &db, "chat_room", &["id", "username"])?;
956        fields.extend(input.fields);
957        input.fields = fields;
958    }
959    if let Some(detail) = &input.detail_fields {
960        check_known_table(&c, "chat_room_info_detail")?;
961        let exists = c
962            .query_row(
963                "SELECT room_id_ FROM chat_room_info_detail WHERE username_=?1",
964                [&input.username],
965                |r| r.get::<_, i64>(0),
966            )
967            .optional()?
968            .is_some();
969        if !exists {
970            let mut fields = nullable_fields(&c, &db, "chat_room_info_detail", &["room_id_"])?;
971            fields.extend(detail.clone());
972            input.detail_fields = Some(fields);
973        }
974    }
975    drop(c);
976    apply_group_projection(location, keys, &input, options)
977}
978
979#[derive(Clone, Debug, Serialize, Deserialize)]
980#[serde(rename_all = "camelCase", deny_unknown_fields)]
981pub struct NativeMemberPatch {
982    pub username: String,
983    pub display: Option<String>,
984    #[serde(default)]
985    pub flags: Option<u32>,
986    #[serde(default)]
987    pub opaque: Option<Vec<u8>>,
988}
989#[derive(Clone, Debug, Serialize, Deserialize)]
990#[serde(rename_all = "camelCase", deny_unknown_fields)]
991pub struct NativeRosterPatch {
992    pub username: String,
993    pub members: Vec<NativeMemberPatch>,
994    pub complete_roster: bool,
995    /// None preserves the saved list. Some replaces it, including an empty list.
996    #[serde(default)]
997    pub administrator_ids: Option<Vec<String>>,
998}
999
1000/// Effects established for ordinary Linux group controls. This writer never
1001/// deletes chat history and never treats a membership delta as a full roster.
1002pub enum NativeGroupEdit<'a> {
1003    RemoveMembers(&'a [String]),
1004    Leave {
1005        own: &'a str,
1006    },
1007    MyName {
1008        own: &'a str,
1009        display: &'a str,
1010    },
1011    Notifications {
1012        muted: bool,
1013        clear_minimized: bool,
1014        own: &'a str,
1015    },
1016    Unfold {
1017        own: String,
1018    },
1019    ClearAnnouncement,
1020}
1021
1022pub fn apply_native_group_edit(
1023    location: &ProfileLocation,
1024    keys: &KeyFile,
1025    room: &str,
1026    edit: NativeGroupEdit<'_>,
1027    options: &WriteOptions,
1028) -> Result<()> {
1029    ownership(location, keys, options)?;
1030    identity(room)?;
1031    if !room.ends_with("@chatroom") {
1032        return Err(ProfileError::InvalidMetadata);
1033    }
1034    let db = find_database(location, "contact.db")?;
1035    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
1036    for table in [
1037        "contact",
1038        "chat_room",
1039        "chatroom_member",
1040        "chat_room_info_detail",
1041    ] {
1042        check_known_table(&c, table)?;
1043    }
1044    let tx = c.transaction()?;
1045    let (id, old): (i64, Option<Vec<u8>>) = tx.query_row(
1046        "SELECT id,ext_buffer FROM chat_room WHERE username=?1",
1047        [room],
1048        |r| Ok((r.get(0)?, r.get(1)?)),
1049    )?;
1050    let mut packed = vec![];
1051    let mut remove = Vec::new();
1052    let mut seen = std::collections::BTreeSet::new();
1053    let mut matched_name = false;
1054    let unfold_own = match &edit {
1055        NativeGroupEdit::Notifications {
1056            own,
1057            clear_minimized: true,
1058            ..
1059        } => Some(*own),
1060        NativeGroupEdit::Unfold { own } => {
1061            identity(own)?;
1062            Some(own.as_str())
1063        }
1064        _ => None,
1065    };
1066    match &edit {
1067        NativeGroupEdit::RemoveMembers(names) => remove.extend_from_slice(names),
1068        NativeGroupEdit::Leave { own } => remove.push((*own).to_owned()),
1069        _ => {}
1070    }
1071    for name in &remove {
1072        identity(name)?;
1073    }
1074    for chunk in proto_chunks(old.as_deref().unwrap_or_default())? {
1075        if chunk.field != 1 {
1076            packed.extend_from_slice(chunk.raw);
1077            continue;
1078        }
1079        if chunk.wire != 2 {
1080            return Err(ProfileError::InvalidMetadata);
1081        }
1082        let name = member_name(chunk.value)?;
1083        if !seen.insert(name.clone()) {
1084            return Err(ProfileError::InvalidMetadata);
1085        }
1086        if remove.contains(&name) {
1087            continue;
1088        }
1089        let updated = match &edit {
1090            NativeGroupEdit::MyName { own, display } if name == *own => {
1091                matched_name = true;
1092                Some(patch_member(
1093                    Some(chunk.value),
1094                    &NativeMemberPatch {
1095                        flags: None,
1096                        opaque: None,
1097                        username: name,
1098                        display: Some((*display).into()),
1099                    },
1100                )?)
1101            }
1102            _ if unfold_own == Some(name.as_str()) => {
1103                let mut member = vec![];
1104                let mut flag_seen = false;
1105                for field in proto_chunks(chunk.value)? {
1106                    if field.field == 3 {
1107                        if field.wire != 0 || flag_seen {
1108                            return Err(ProfileError::InvalidMetadata);
1109                        }
1110                        flag_seen = true;
1111                        let mut at = 0;
1112                        let value = proto_var(field.value, &mut at)?;
1113                        var_out(3 << 3, &mut member);
1114                        var_out(value & !0x2000, &mut member);
1115                    } else {
1116                        member.extend_from_slice(field.raw);
1117                    }
1118                }
1119                Some(member)
1120            }
1121            _ => None,
1122        };
1123        if let Some(member) = updated {
1124            proto_bytes(1, &member, &mut packed);
1125        } else {
1126            packed.extend_from_slice(chunk.raw);
1127        }
1128    }
1129    if matches!(edit, NativeGroupEdit::MyName { .. }) && !matched_name {
1130        return Err(ProfileError::InvalidMetadata);
1131    }
1132    if packed != old.as_deref().unwrap_or_default() {
1133        tx.execute(
1134            "UPDATE chat_room SET ext_buffer=?2 WHERE id=?1",
1135            rusqlite::params![id, packed],
1136        )?;
1137    }
1138    for name in &remove {
1139        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])?;
1140    }
1141    match edit {
1142        NativeGroupEdit::Leave { .. } => {
1143            if tx.execute("UPDATE contact SET is_in_chat_room=2 WHERE username=?1", [room])? != 1 { return Err(ProfileError::InvalidMetadata); }
1144        }
1145        NativeGroupEdit::Notifications { muted, clear_minimized, .. } => {
1146            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); }
1147        }
1148        NativeGroupEdit::Unfold { .. } => {
1149            let flag: Option<i64> = tx.query_row("SELECT flag FROM contact WHERE username=?1 AND local_type=2 AND delete_flag=0 AND is_in_chat_room=1", [room], |r| r.get(0)).optional()?.flatten();
1150            let flag = flag.and_then(|flag| u32::try_from(flag).ok()).filter(|flag| flag & 2 != 0).ok_or(ProfileError::InvalidMetadata)?;
1151            if tx.execute("UPDATE contact SET flag=?2 WHERE username=?1", rusqlite::params![room,flag & !0x10000000])? != 1 { return Err(ProfileError::InvalidMetadata); }
1152        }
1153        NativeGroupEdit::ClearAnnouncement => {
1154            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])?;
1155        }
1156        _ => {}
1157    }
1158    tx.commit()?;
1159    Ok(())
1160}
1161struct ProtoChunk<'a> {
1162    field: u64,
1163    wire: u8,
1164    raw: &'a [u8],
1165    value: &'a [u8],
1166}
1167fn proto_chunks(data: &[u8]) -> Result<Vec<ProtoChunk<'_>>> {
1168    if data.len() > 1024 * 1024 {
1169        return Err(ProfileError::ResourceLimit);
1170    }
1171    let mut at = 0;
1172    let mut chunks = vec![];
1173    while at < data.len() {
1174        let start = at;
1175        let tag = proto_var(data, &mut at)?;
1176        let field = tag >> 3;
1177        let wire = (tag & 7) as u8;
1178        if field == 0 || field >= 1 << 29 {
1179            return Err(ProfileError::InvalidMetadata);
1180        }
1181        let mut value_start = at;
1182        match wire {
1183            0 => {
1184                proto_var(data, &mut at)?;
1185            }
1186            1 => at = at.checked_add(8).ok_or(ProfileError::ResourceLimit)?,
1187            5 => at = at.checked_add(4).ok_or(ProfileError::ResourceLimit)?,
1188            2 => {
1189                let size = usize::try_from(proto_var(data, &mut at)?)
1190                    .map_err(|_| ProfileError::ResourceLimit)?;
1191                value_start = at;
1192                at = at.checked_add(size).ok_or(ProfileError::ResourceLimit)?
1193            }
1194            _ => return Err(ProfileError::InvalidMetadata),
1195        }
1196        let raw = data.get(start..at).ok_or(ProfileError::InvalidMetadata)?;
1197        let value = data
1198            .get(value_start..at)
1199            .ok_or(ProfileError::InvalidMetadata)?;
1200        chunks.push(ProtoChunk {
1201            field,
1202            wire,
1203            raw,
1204            value,
1205        });
1206        if chunks.len() > 65536 {
1207            return Err(ProfileError::ResourceLimit);
1208        }
1209    }
1210    Ok(chunks)
1211}
1212fn proto_bytes(field: u64, value: &[u8], out: &mut Vec<u8>) {
1213    var_out((field << 3) | 2, out);
1214    var_out(value.len() as u64, out);
1215    out.extend_from_slice(value)
1216}
1217fn member_name(data: &[u8]) -> Result<String> {
1218    let mut name = None;
1219    let mut display_seen = false;
1220    for chunk in proto_chunks(data)? {
1221        match chunk.field {
1222            1 => {
1223                if chunk.wire != 2 || name.is_some() {
1224                    return Err(ProfileError::InvalidMetadata);
1225                }
1226                let value =
1227                    std::str::from_utf8(chunk.value).map_err(|_| ProfileError::InvalidMetadata)?;
1228                identity(value)?;
1229                name = Some(value.to_owned());
1230            }
1231            2 => {
1232                if chunk.wire != 2 || display_seen {
1233                    return Err(ProfileError::InvalidMetadata);
1234                }
1235                std::str::from_utf8(chunk.value).map_err(|_| ProfileError::InvalidMetadata)?;
1236                display_seen = true;
1237            }
1238            _ => {}
1239        }
1240    }
1241    name.ok_or(ProfileError::InvalidMetadata)
1242}
1243fn patch_member(old: Option<&[u8]>, patch: &NativeMemberPatch) -> Result<Vec<u8>> {
1244    identity(&patch.username)?;
1245    if patch.opaque.as_ref().is_some_and(|b| b.len() > 65536) {
1246        return Err(ProfileError::ResourceLimit);
1247    }
1248    if patch
1249        .display
1250        .as_ref()
1251        .is_some_and(|s| s.len() > 65536 || s.contains('\0'))
1252    {
1253        return Err(ProfileError::InvalidMetadata);
1254    }
1255    let mut out = vec![];
1256    let mut wrote_display = false;
1257    let mut wrote_flags = false;
1258    let mut wrote_opaque = false;
1259    if let Some(old) = old {
1260        if member_name(old)? != patch.username {
1261            return Err(ProfileError::InvalidMetadata);
1262        }
1263        for chunk in proto_chunks(old)? {
1264            if chunk.field == 2 {
1265                if let Some(display) = &patch.display {
1266                    proto_bytes(2, display.as_bytes(), &mut out);
1267                    wrote_display = true;
1268                } else {
1269                    out.extend_from_slice(chunk.raw);
1270                }
1271            } else if chunk.field == 3 && patch.flags.is_some() {
1272                if chunk.wire != 0 || wrote_flags {
1273                    return Err(ProfileError::InvalidMetadata);
1274                }
1275                var_out(3 << 3, &mut out);
1276                var_out(u64::from(patch.flags.unwrap()), &mut out);
1277                wrote_flags = true;
1278            } else if chunk.field == 4 && patch.opaque.is_some() {
1279                if chunk.wire != 2 || wrote_opaque {
1280                    return Err(ProfileError::InvalidMetadata);
1281                }
1282                proto_bytes(4, patch.opaque.as_deref().unwrap(), &mut out);
1283                wrote_opaque = true;
1284            } else {
1285                out.extend_from_slice(chunk.raw);
1286            }
1287        }
1288    } else {
1289        proto_bytes(1, patch.username.as_bytes(), &mut out);
1290    }
1291    if !wrote_display {
1292        if let Some(display) = &patch.display {
1293            proto_bytes(2, display.as_bytes(), &mut out)
1294        }
1295    }
1296    if !wrote_flags {
1297        if let Some(flags) = patch.flags {
1298            var_out(3 << 3, &mut out);
1299            var_out(u64::from(flags), &mut out);
1300        }
1301    }
1302    if !wrote_opaque {
1303        if let Some(opaque) = &patch.opaque {
1304            proto_bytes(4, opaque, &mut out);
1305        }
1306    }
1307    Ok(out)
1308}
1309/// Translate only the proved network member identity/display semantics into the
1310/// local repeated-field1 representation. Caller proves roster completeness (for
1311/// example, a separate complete member identity list in the server reply). Unknown
1312/// root/member protobuf chunks, existing owner and unrelated contact fields are
1313/// retained byte-for-byte. No network version is guessed into opaque fields.
1314pub fn apply_native_roster_patch(
1315    location: &ProfileLocation,
1316    keys: &KeyFile,
1317    input: &NativeRosterPatch,
1318    options: &WriteOptions,
1319) -> Result<()> {
1320    ownership(location, keys, options)?;
1321    identity(&input.username)?;
1322    if !input.username.ends_with("@chatroom") || input.members.len() > 10000 {
1323        return Err(ProfileError::InvalidMetadata);
1324    }
1325    let mut patches = std::collections::BTreeMap::new();
1326    if let Some(ids) = &input.administrator_ids {
1327        let mut seen = std::collections::BTreeSet::new();
1328        for id in ids {
1329            identity(id)?;
1330            if !seen.insert(id) || seen.len() > 10000 {
1331                return Err(ProfileError::InvalidMetadata);
1332            }
1333        }
1334    }
1335    for member in &input.members {
1336        identity(&member.username)?;
1337        if patches.insert(member.username.clone(), member).is_some() {
1338            return Err(ProfileError::InvalidMetadata);
1339        }
1340    }
1341    let db = find_database(location, "contact.db")?;
1342    let c = open_write(location, &db, keys, options.allow_plaintext)?;
1343    check_known_table(&c, "chat_room")?;
1344    check_known_table(&c, "chatroom_member")?;
1345    let room = c
1346        .query_row(
1347            "SELECT id,ext_buffer FROM chat_room WHERE username=?1",
1348            [&input.username],
1349            |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Option<Vec<u8>>>(1)?)),
1350        )
1351        .optional()?;
1352    let old = room
1353        .as_ref()
1354        .and_then(|(_, bytes)| bytes.as_deref())
1355        .unwrap_or(&[]);
1356    let chunks = proto_chunks(old)?;
1357    let mut old_members = std::collections::BTreeMap::new();
1358    for chunk in &chunks {
1359        if chunk.field == 1 {
1360            if chunk.wire != 2 {
1361                return Err(ProfileError::InvalidMetadata);
1362            }
1363            let name = member_name(chunk.value)?;
1364            if old_members.insert(name, chunk.value).is_some() {
1365                return Err(ProfileError::InvalidMetadata);
1366            }
1367        }
1368    }
1369    let mut result_members = if input.complete_roster {
1370        std::collections::BTreeSet::new()
1371    } else {
1372        old_members.keys().cloned().collect()
1373    };
1374    if !input.complete_roster {
1375        if let Some((id, _)) = &room {
1376            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<_>,_>>()?;
1377            result_members.extend(names);
1378        }
1379    }
1380    result_members.extend(patches.keys().cloned());
1381    let mut packed = vec![];
1382    let mut written = std::collections::BTreeSet::new();
1383    for chunk in chunks {
1384        if chunk.field == 5 && input.administrator_ids.is_some() {
1385            if chunk.wire != 2 {
1386                return Err(ProfileError::InvalidMetadata);
1387            }
1388            continue;
1389        }
1390        if chunk.field != 1 {
1391            packed.extend_from_slice(chunk.raw);
1392            continue;
1393        }
1394        let name = member_name(chunk.value)?;
1395        if !result_members.contains(&name) {
1396            continue;
1397        }
1398        if let Some(patch) = patches.get(&name) {
1399            proto_bytes(1, &patch_member(Some(chunk.value), patch)?, &mut packed)
1400        } else {
1401            packed.extend_from_slice(chunk.raw)
1402        }
1403        written.insert(name);
1404    }
1405    // New members have no invented opaque metadata. Their contact rows must already
1406    // exist; the SQL projection checks that binding and allocates destination IDs.
1407    for name in result_members.difference(&written) {
1408        let empty = NativeMemberPatch {
1409            flags: None,
1410            opaque: None,
1411            username: name.clone(),
1412            display: None,
1413        };
1414        let patch = patches.get(name).copied().unwrap_or(&empty);
1415        proto_bytes(1, &patch_member(None, patch)?, &mut packed);
1416    }
1417    let mut fields = RawFields::new();
1418    if let Some(ids) = &input.administrator_ids {
1419        for id in ids {
1420            proto_bytes(5, id.as_bytes(), &mut packed);
1421        }
1422    }
1423    fields.insert("ext_buffer".into(), SqlValue::Blob(STANDARD.encode(packed)));
1424    drop(c);
1425    apply_group_patch(
1426        location,
1427        keys,
1428        &GroupProjection {
1429            username: input.username.clone(),
1430            fields,
1431            detail_fields: None,
1432            members: result_members.into_iter().collect(),
1433            complete_roster: input.complete_roster,
1434        },
1435        options,
1436    )
1437}
1438
1439/// WCDB compression state 4 observed on the supported fields. Compression is
1440/// independent of media type; raw protocol bytes stay in the durable sidecar.
1441pub fn compressed_profile_field(bytes: &[u8]) -> Result<SqlValue> {
1442    if bytes.len() > 4 * 1024 * 1024 {
1443        return Err(ProfileError::ResourceLimit);
1444    }
1445    let compressed =
1446        zstd::stream::encode_all(bytes, 3).map_err(|_| ProfileError::InvalidMetadata)?;
1447    Ok(SqlValue::Blob(STANDARD.encode(compressed)))
1448}
1449/// Find one exact server identity in the sole supported ordinary shard. This
1450/// never adopts a first match from an ambiguous or rotated shard family.
1451pub fn find_projected_message(
1452    location: &ProfileLocation,
1453    keys: &KeyFile,
1454    peer: &str,
1455    server_id: &str,
1456    options: &WriteOptions,
1457) -> Result<Option<SourceMessageId>> {
1458    ownership(location, keys, options)?;
1459    identity(peer)?;
1460    crate::writer::require_single_message_shard(location)?;
1461    let sid = server_id
1462        .parse::<u64>()
1463        .map_err(|_| ProfileError::InvalidMetadata)?;
1464    if sid == 0 {
1465        return Err(ProfileError::InvalidMetadata);
1466    }
1467    let db = find_database(location, "message_0.db")?;
1468    let c = open_write(location, &db, keys, options.allow_plaintext)?;
1469    let table = format!("Msg_{:x}", Md5::digest(peer.as_bytes()));
1470    let present = c.query_row(
1471        "SELECT count(*)FROM sqlite_master WHERE type='table' AND name=?1",
1472        [&table],
1473        |r| r.get::<_, i64>(0),
1474    )?;
1475    if present == 0 {
1476        return Ok(None);
1477    }
1478    crate::writer::exact_columns(&c, &db, &table, crate::writer::MESSAGE_COLUMNS)?;
1479    let ids = c
1480        .prepare(&format!(
1481            "SELECT local_id FROM {} WHERE server_id=?1 LIMIT 2",
1482            quote_identifier(&table)
1483        ))?
1484        .query_map([sid as i64], |r| r.get::<_, i64>(0))?
1485        .collect::<std::result::Result<Vec<_>, _>>()?;
1486    if ids.len() > 1 {
1487        return Err(ProfileError::InvalidMetadata);
1488    }
1489    Ok(ids.first().map(|id| SourceMessageId {
1490        database: db,
1491        table,
1492        local_id: id.to_string(),
1493    }))
1494}
1495/// Choose the first unused millisecond slot in this message's second, or retain
1496/// the already stored slot for a replay. This is an independent collision policy
1497/// within the observed native 1000-slots/second representation, not a recovered
1498/// native rollover algorithm. Caller holds the lease through insertion.
1499pub fn choose_message_sort_sequence(
1500    location: &ProfileLocation,
1501    keys: &KeyFile,
1502    peer: &str,
1503    server_id: &str,
1504    time: i64,
1505    options: &WriteOptions,
1506) -> Result<i64> {
1507    ownership(location, keys, options)?;
1508    identity(peer)?;
1509    if time < 0 {
1510        return Err(ProfileError::InvalidMetadata);
1511    }
1512    let base = time.checked_mul(1000).ok_or(ProfileError::ResourceLimit)?;
1513    let end = base.checked_add(999).ok_or(ProfileError::ResourceLimit)?;
1514    let db = find_database(location, "message_0.db")?;
1515    let c = open_write(location, &db, keys, options.allow_plaintext)?;
1516    let table = format!("Msg_{:x}", Md5::digest(peer.as_bytes()));
1517    if c.query_row(
1518        "SELECT count(*)FROM sqlite_master WHERE type='table' AND name=?1",
1519        [&table],
1520        |r| r.get::<_, i64>(0),
1521    )? == 0
1522    {
1523        return Ok(base);
1524    }
1525    if let Some(id) = find_projected_message(location, keys, peer, server_id, options)? {
1526        let value = c.query_row(
1527            &format!(
1528                "SELECT create_time,sort_seq FROM {} WHERE local_id=?1",
1529                quote_identifier(&table)
1530            ),
1531            [id.local_id
1532                .parse::<i64>()
1533                .map_err(|_| ProfileError::InvalidMetadata)?],
1534            |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
1535        )?;
1536        if value.0 != time {
1537            return Err(ProfileError::InvalidMetadata);
1538        }
1539        return Ok(value.1);
1540    }
1541    let used = c
1542        .prepare(&format!(
1543            "SELECT sort_seq FROM {} WHERE sort_seq BETWEEN ?1 AND ?2",
1544            quote_identifier(&table)
1545        ))?
1546        .query_map(params![base, end], |r| r.get::<_, i64>(0))?
1547        .collect::<std::result::Result<std::collections::BTreeSet<_>, _>>()?;
1548    (base..=end)
1549        .find(|n| !used.contains(n))
1550        .ok_or(ProfileError::ResourceLimit)
1551}
1552#[derive(Clone, Debug, Serialize, Deserialize)]
1553#[serde(rename_all = "camelCase", deny_unknown_fields)]
1554pub struct MessageSessionProjection {
1555    pub source: SourceMessageId,
1556    pub peer: String,
1557    pub summary: String,
1558    pub outgoing: bool,
1559    pub unread: bool,
1560    pub sender: Option<String>,
1561    pub sender_display: Option<String>,
1562    /// Only a durable coordinator intent can establish that an existing row came
1563    /// from this unfinished projection. Imported/adopted rows never gain unread.
1564    pub created_by_this_intent: bool,
1565}
1566/// Reconcile a committed message into a desktop session, preserving drafts,
1567/// unrelated unread rows and preexisting counter/list discrepancies. It makes no
1568/// server/phone read request and is retryable after a message-only commit.
1569pub fn apply_message_session_projection(
1570    location: &ProfileLocation,
1571    keys: &KeyFile,
1572    input: &MessageSessionProjection,
1573    options: &WriteOptions,
1574) -> Result<()> {
1575    ownership(location, keys, options)?;
1576    identity(&input.peer)?;
1577    if input.summary.len() > 4 * 1024 * 1024
1578        || input.source.table != format!("Msg_{:x}", Md5::digest(input.peer.as_bytes()))
1579    {
1580        return Err(ProfileError::InvalidMetadata);
1581    }
1582    let messages = open_write(
1583        location,
1584        &input.source.database,
1585        keys,
1586        options.allow_plaintext,
1587    )?;
1588    crate::writer::exact_columns(
1589        &messages,
1590        &input.source.database,
1591        &input.source.table,
1592        crate::writer::MESSAGE_COLUMNS,
1593    )?;
1594    let local = input
1595        .source
1596        .local_id
1597        .parse::<i64>()
1598        .map_err(|_| ProfileError::InvalidMetadata)?;
1599    let (server, time, kind) = messages.query_row(
1600        &format!(
1601            "SELECT server_id,create_time,local_type FROM {} WHERE local_id=?1",
1602            quote_identifier(&input.source.table)
1603        ),
1604        [local],
1605        |r| {
1606            Ok((
1607                r.get::<_, i64>(0)?,
1608                r.get::<_, i64>(1)?,
1609                r.get::<_, i64>(2)?,
1610            ))
1611        },
1612    )?;
1613    drop(messages);
1614    let db = find_database(location, "session.db")?;
1615    let mut c = open_write(location, &db, keys, options.allow_plaintext)?;
1616    for t in [
1617        "SessionTable",
1618        "SessionUnreadListTable_1",
1619        "SessionUnreadStatTable_1",
1620    ] {
1621        check_known_table(&c, t)?;
1622    }
1623    let tx = c.transaction()?;
1624    let session = map_id(&tx, "Name2Id", "user_name", &input.peer)?;
1625    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()?;
1626    if old.is_none() {
1627        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])?;
1628    }
1629    let old = old.unwrap_or((0, 0, 0, 0));
1630    if old.0 < 0 {
1631        return Err(ProfileError::InvalidMetadata);
1632    }
1633    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()?;
1634    if unread.is_some_and(|v| v != (local, time)) {
1635        return Err(ProfileError::InvalidMetadata);
1636    }
1637    let done = old.1 == local && old.2 == time;
1638    if !input.outgoing
1639        && input.unread
1640        && input.created_by_this_intent
1641        && unread.is_none()
1642        && !done
1643        && old.3 < time
1644    {
1645        // A newer sidebar summary is not a read marker: sync may arrive out of
1646        // order. Only an intent-owned new row and the explicit local clear cutoff
1647        // permit this addition; imported/adopted rows never regain unread.
1648        tx.execute("INSERT INTO SessionUnreadListTable_1(username_id,server_id,local_id,create_time)VALUES(?1,?2,?3,?4)",params![session,server,local,time])?;
1649        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])?;
1650    }
1651    if old.2 <= time && (old.2 < time || old.1 <= local) {
1652        let sender = if input.peer.ends_with("@chatroom") {
1653            input.sender.as_deref().unwrap_or("")
1654        } else {
1655            ""
1656        };
1657        let display = if input.peer.ends_with("@chatroom") {
1658            input.sender_display.as_deref().unwrap_or("")
1659        } else {
1660            ""
1661        };
1662        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])?;
1663    }
1664    tx.commit()?;
1665    Ok(())
1666}
1667
1668#[cfg(test)]
1669mod pin_tests {
1670    use super::*;
1671
1672    fn fixture(encrypted: bool) -> (tempfile::TempDir, ProfileLocation, KeyFile, WriteOptions) {
1673        let temp = tempfile::tempdir().unwrap();
1674        let root = temp.path().join("account");
1675        let (location, keys) = if encrypted {
1676            let (location, keys, _) = initialize_profile(&root, &[42; 32]).unwrap();
1677            (location, keys)
1678        } else {
1679            for (relative, sql) in [
1680                ("contact/contact.db", include_str!("layout/contact.db.sql")),
1681                ("session/session.db", include_str!("layout/session.db.sql")),
1682            ] {
1683                let path = root.join("db_storage").join(relative);
1684                std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1685                Connection::open(path).unwrap().execute_batch(sql).unwrap();
1686            }
1687            (
1688                ProfileLocation::new(&root).unwrap(),
1689                KeyFile::empty("account"),
1690            )
1691        };
1692        let options = WriteOptions {
1693            exclusive_ownership_confirmed: true,
1694            allow_plaintext: !encrypted,
1695            durable_intent_id: "synthetic-pin-receipt".into(),
1696        };
1697        (temp, location, keys, options)
1698    }
1699
1700    #[test]
1701    fn pin_projection_preserves_other_state_and_replays_without_rewinding_activity() {
1702        for encrypted in [false, true] {
1703            let (_temp, location, keys, options) = fixture(encrypted);
1704            let contact = open_write(&location, "contact/contact.db", &keys, !encrypted).unwrap();
1705            let session = open_write(&location, "session/session.db", &keys, !encrypted).unwrap();
1706            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,0,0,0),(2,'room@chatroom',268435586,2,0,1,0);").unwrap();
1707            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();
1708            let saved_flag = || {
1709                contact
1710                    .query_row(
1711                        "SELECT flag FROM contact WHERE username='peer'",
1712                        [],
1713                        |row| row.get::<_, i64>(0),
1714                    )
1715                    .unwrap()
1716            };
1717            let saved_sort = || {
1718                session
1719                    .query_row(
1720                        "SELECT sort_timestamp FROM SessionTable WHERE username='peer'",
1721                        [],
1722                        |row| row.get::<_, i64>(0),
1723                    )
1724                    .unwrap()
1725            };
1726            let baseline = saved_flag();
1727            let result =
1728                apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &options)
1729                    .unwrap()
1730                    .unwrap();
1731            assert!(result.changed && result.session_projected);
1732            assert!(!result.cross_database_atomic);
1733            assert_eq!(saved_flag(), baseline | 0x800);
1734            assert_eq!(saved_sort(), 200);
1735            assert!(
1736                !apply_contact_pin_patch(&location, &keys, "peer", true, false, 300, &options)
1737                    .unwrap()
1738                    .unwrap()
1739                    .changed
1740            );
1741            assert_eq!(saved_sort(), 200);
1742            session
1743                .execute(
1744                    "UPDATE SessionTable SET sort_timestamp=400 WHERE username='peer'",
1745                    [],
1746                )
1747                .unwrap();
1748            apply_contact_pin_patch(&location, &keys, "peer", false, false, 250, &options)
1749                .unwrap()
1750                .unwrap();
1751            assert_eq!(saved_flag(), baseline);
1752            assert_eq!(saved_sort(), 400);
1753            // Model an interrupted session-first commit before its contact flag.
1754            session
1755                .execute(
1756                    "UPDATE SessionTable SET sort_timestamp=500 WHERE username='peer'",
1757                    [],
1758                )
1759                .unwrap();
1760            apply_contact_pin_patch(&location, &keys, "peer", true, true, 450, &options)
1761                .unwrap()
1762                .unwrap();
1763            assert_eq!(saved_flag(), (baseline & !0x10000080) | 0x800);
1764            assert_eq!(saved_sort(), 500);
1765            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();
1766            assert_eq!(unrelated, (4, "Keep draft".into(), 7, 0, 90, 12));
1767            apply_contact_pin_patch(&location, &keys, "room@chatroom", true, true, 600, &options)
1768                .unwrap()
1769                .unwrap();
1770            assert_eq!(
1771                contact
1772                    .query_row(
1773                        "SELECT flag,chat_room_notify FROM contact WHERE username='room@chatroom'",
1774                        [],
1775                        |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1776                    )
1777                    .unwrap(),
1778                (2050, 0)
1779            );
1780            apply_contact_pin_patch(
1781                &location,
1782                &keys,
1783                "room@chatroom",
1784                false,
1785                false,
1786                700,
1787                &options,
1788            )
1789            .unwrap()
1790            .unwrap();
1791            assert_eq!(
1792                session
1793                    .query_row(
1794                        "SELECT sort_timestamp FROM SessionTable WHERE username='room@chatroom'",
1795                        [],
1796                        |row| row.get::<_, i64>(0)
1797                    )
1798                    .unwrap(),
1799                700
1800            );
1801        }
1802    }
1803
1804    #[test]
1805    fn pin_projection_preflights_both_schemas_and_keeps_wal_modes() {
1806        let (_temp, location, keys, options) = fixture(false);
1807        let contact = open_write(&location, "contact/contact.db", &keys, true).unwrap();
1808        let session = open_write(&location, "session/session.db", &keys, true).unwrap();
1809        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();
1810        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();
1811        for name in [
1812            "missing",
1813            "no_session",
1814            "missing_flag",
1815            "filehelper",
1816            "left@chatroom",
1817        ] {
1818            assert!(
1819                apply_contact_pin_patch(&location, &keys, name, true, false, 200, &options)
1820                    .unwrap()
1821                    .is_none(),
1822                "{name}"
1823            );
1824        }
1825        session
1826            .execute_batch("ALTER TABLE SessionTable ADD COLUMN unknown_column TEXT;")
1827            .unwrap();
1828        assert!(matches!(
1829            apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &options),
1830            Err(ProfileError::UnsupportedSchema(_))
1831        ));
1832        assert_eq!(
1833            contact
1834                .query_row(
1835                    "SELECT flag FROM contact WHERE username='peer'",
1836                    [],
1837                    |row| row.get::<_, i64>(0)
1838                )
1839                .unwrap(),
1840            1
1841        );
1842        assert_eq!(
1843            session
1844                .query_row(
1845                    "SELECT sort_timestamp FROM SessionTable WHERE username='peer'",
1846                    [],
1847                    |row| row.get::<_, i64>(0)
1848                )
1849                .unwrap(),
1850            100
1851        );
1852        session
1853            .execute_batch("ALTER TABLE SessionTable DROP COLUMN unknown_column;")
1854            .unwrap();
1855        let mut unowned = options.clone();
1856        unowned.exclusive_ownership_confirmed = false;
1857        assert!(matches!(
1858            apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &unowned),
1859            Err(ProfileError::SnapshotRequired)
1860        ));
1861        assert!(
1862            apply_contact_pin_patch(&location, &keys, "peer", false, true, 200, &options).is_err()
1863        );
1864        apply_contact_pin_patch(&location, &keys, "peer", true, false, 200, &options)
1865            .unwrap()
1866            .unwrap();
1867        for db in [&contact, &session] {
1868            assert_eq!(
1869                db.query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))
1870                    .unwrap(),
1871                "wal"
1872            );
1873        }
1874    }
1875
1876    #[test]
1877    fn group_unfold_preserves_notification_state_other_members_and_opaque_fields() {
1878        let (_temp, location, keys, options) = fixture(false);
1879        let contact = open_write(&location, "contact/contact.db", &keys, true).unwrap();
1880        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,0,1,0);INSERT INTO chat_room(id,username)VALUES(1,'room@chatroom');").unwrap();
1881        let before = [
1882            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',
1883            b'e', b'e', b'r', 24, 129, 64, 42, 3, 9, 8, 7,
1884        ];
1885        let expected = [
1886            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',
1887            b'e', b'r', 24, 129, 64, 42, 3, 9, 8, 7,
1888        ];
1889        contact
1890            .execute("UPDATE chat_room SET ext_buffer=?1", [before.as_slice()])
1891            .unwrap();
1892        for _ in 0..2 {
1893            apply_native_group_edit(
1894                &location,
1895                &keys,
1896                "room@chatroom",
1897                NativeGroupEdit::Unfold { own: "self".into() },
1898                &options,
1899            )
1900            .unwrap();
1901        }
1902        assert_eq!(
1903            contact
1904                .query_row(
1905                    "SELECT flag,chat_room_notify FROM contact WHERE username='room@chatroom'",
1906                    [],
1907                    |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1908                )
1909                .unwrap(),
1910            (2, 0)
1911        );
1912        assert_eq!(
1913            contact
1914                .query_row("SELECT ext_buffer FROM chat_room", [], |row| row
1915                    .get::<_, Vec<u8>>(0))
1916                .unwrap(),
1917            expected
1918        );
1919        // Invalid contact metadata rolls back even the already prepared packed edit.
1920        contact
1921            .execute("UPDATE contact SET flag=4294967296", [])
1922            .unwrap();
1923        contact
1924            .execute("UPDATE chat_room SET ext_buffer=?1", [before.as_slice()])
1925            .unwrap();
1926        assert!(apply_native_group_edit(
1927            &location,
1928            &keys,
1929            "room@chatroom",
1930            NativeGroupEdit::Unfold { own: "self".into() },
1931            &options
1932        )
1933        .is_err());
1934        assert_eq!(
1935            contact
1936                .query_row("SELECT ext_buffer FROM chat_room", [], |row| row
1937                    .get::<_, Vec<u8>>(0))
1938                .unwrap(),
1939            before
1940        );
1941    }
1942}