1use std::collections::HashMap;
15use std::ffi::{c_char, c_int, CStr, CString};
16use std::ptr;
17
18use libsqlite3_sys as ffi;
19
20use super::conflict::{lww_conflict_handler, ConflictTracker, TableSchema};
21use super::session::SyncError;
22use super::session_ext::{apply_changeset_with_context, Changeset};
23
24pub struct ApplyResult {
26 pub had_fk_violations: bool,
30}
31
32struct DeviceLocalSnapshot {
34 encryption_nonce: Option<Vec<u8>>,
36}
37
38pub unsafe fn apply_changeset_lww(
47 db: *mut ffi::sqlite3,
48 changeset: &Changeset,
49) -> Result<ApplyResult, SyncError> {
50 let synced = super::session::synced_tables();
51 let table_refs: Vec<&str> = synced.iter().map(String::as_str).collect();
52 let schema = TableSchema::from_db(db, &table_refs);
53 let mut tracker = ConflictTracker::new();
54
55 let snapshots = snapshot_device_local_columns(db);
59
60 apply_changeset_with_context(db, changeset, |ct, ctx| {
61 lww_conflict_handler(ct, ctx, &schema, &mut tracker)
62 })
63 .map_err(SyncError::ChangesetApply)?;
64
65 for row_id in &tracker.release_file_restore_ids {
67 if let Some(snap) = snapshots.get(row_id.as_str()) {
68 restore_device_local_columns(db, row_id, snap);
69 }
70 }
71
72 Ok(ApplyResult {
73 had_fk_violations: tracker.had_constraint_conflict,
74 })
75}
76
77unsafe fn snapshot_device_local_columns(
79 db: *mut ffi::sqlite3,
80) -> HashMap<String, DeviceLocalSnapshot> {
81 let mut map = HashMap::new();
82
83 let sql = "SELECT id, encryption_nonce FROM release_files";
84 let c_sql = CString::new(sql).unwrap();
85 let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
86 let rc = ffi::sqlite3_prepare_v2(db, c_sql.as_ptr(), -1, &mut stmt, ptr::null_mut());
87
88 if rc != ffi::SQLITE_OK as c_int {
91 return map;
92 }
93
94 while ffi::sqlite3_step(stmt) == ffi::SQLITE_ROW as c_int {
95 let id_ptr = ffi::sqlite3_column_text(stmt, 0);
97 if id_ptr.is_null() {
98 continue;
99 }
100 let id = CStr::from_ptr(id_ptr as *const c_char)
101 .to_str()
102 .expect("SQLite text columns are always UTF-8")
103 .to_string();
104
105 let encryption_nonce = if ffi::sqlite3_column_type(stmt, 1) == ffi::SQLITE_NULL as c_int {
107 None
108 } else {
109 let blob_ptr = ffi::sqlite3_column_blob(stmt, 1);
110 let blob_len = ffi::sqlite3_column_bytes(stmt, 1) as usize;
111 if blob_ptr.is_null() || blob_len == 0 {
112 None
113 } else {
114 let slice = std::slice::from_raw_parts(blob_ptr as *const u8, blob_len);
115 Some(slice.to_vec())
116 }
117 };
118
119 map.insert(id, DeviceLocalSnapshot { encryption_nonce });
120 }
121
122 ffi::sqlite3_finalize(stmt);
123 map
124}
125
126unsafe fn restore_device_local_columns(
129 db: *mut ffi::sqlite3,
130 row_id: &str,
131 snap: &DeviceLocalSnapshot,
132) {
133 let sql = "UPDATE release_files SET encryption_nonce = ?1 WHERE id = ?2";
134 let c_sql = CString::new(sql).unwrap();
135 let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
136 let rc = ffi::sqlite3_prepare_v2(db, c_sql.as_ptr(), -1, &mut stmt, ptr::null_mut());
137 assert_eq!(
138 rc,
139 ffi::SQLITE_OK as c_int,
140 "prepare restore_device_local_columns failed"
141 );
142
143 match &snap.encryption_nonce {
145 Some(bytes) => {
146 ffi::sqlite3_bind_blob(
147 stmt,
148 1,
149 bytes.as_ptr() as *const _,
150 bytes.len() as c_int,
151 ffi::SQLITE_TRANSIENT(),
152 );
153 }
154 None => {
155 ffi::sqlite3_bind_null(stmt, 1);
156 }
157 }
158
159 let c_id = CString::new(row_id).unwrap();
161 ffi::sqlite3_bind_text(
162 stmt,
163 2,
164 c_id.as_ptr(),
165 row_id.len() as c_int,
166 ffi::SQLITE_TRANSIENT(),
167 );
168
169 let step = ffi::sqlite3_step(stmt);
170 assert_eq!(
171 step,
172 ffi::SQLITE_DONE as c_int,
173 "restore_device_local_columns step failed"
174 );
175
176 ffi::sqlite3_finalize(stmt);
177}