appcore_sync_sqlite/
checkpoint.rs1use crate::{SqliteSyncError, SqliteSyncStore};
12use appcore_sync::{SyncCheckpointStore, SyncError, SyncResult};
13use rusqlite::{params, OptionalExtension, TransactionBehavior};
14
15const MAX_PEER_ID_BYTES: usize = 256;
16
17#[derive(Debug, Clone)]
19pub struct SqliteSyncCheckpointStore {
20 store: SqliteSyncStore,
21}
22
23impl SqliteSyncCheckpointStore {
24 pub(crate) fn new(store: SqliteSyncStore) -> Self {
25 Self { store }
26 }
27}
28
29impl SyncCheckpointStore for SqliteSyncCheckpointStore {
30 fn get_checkpoint(&self, peer_id: &str) -> SyncResult<Option<(u64, String)>> {
31 validate_peer_id(peer_id)?;
32 self.store
33 .with_connection(|connection| {
34 connection
35 .query_row(
36 "SELECT sequence, batch_hash FROM appcore_sync_checkpoint
37 WHERE peer_id = ?1",
38 [peer_id],
39 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
40 )
41 .optional()
42 .map_err(SqliteSyncError::database)?
43 .map(|(sequence, hash)| {
44 u64::try_from(sequence)
45 .map(|sequence| (sequence, hash))
46 .map_err(|_| SqliteSyncError::CorruptRecord("checkpoint"))
47 })
48 .transpose()
49 })
50 .map_err(SqliteSyncError::sync)
51 }
52
53 fn set_checkpoint(&self, peer_id: &str, sequence: u64, hash: &str) -> SyncResult<()> {
54 validate_peer_id(peer_id)?;
55 validate_hash(hash)?;
56 let sequence_u64 = sequence;
57 let sequence = i64::try_from(sequence).map_err(|_| {
58 SyncError::ReplicationFailed("checkpoint capacity exceeded".to_string())
59 })?;
60 self.store
61 .with_connection(|connection| {
62 let transaction = connection
63 .transaction_with_behavior(TransactionBehavior::Immediate)
64 .map_err(SqliteSyncError::database)?;
65 let existing: Option<(i64, String)> = transaction
66 .query_row(
67 "SELECT sequence, batch_hash FROM appcore_sync_checkpoint WHERE peer_id = ?1",
68 [peer_id],
69 |row| Ok((row.get(0)?, row.get(1)?)),
70 )
71 .optional()
72 .map_err(SqliteSyncError::database)?;
73 if let Some((current_sequence, current_hash)) = existing {
74 if sequence < current_sequence
75 || (sequence == current_sequence && hash != current_hash)
76 {
77 return Err(SqliteSyncError::CorruptRecord("checkpoint conflict"));
78 }
79 if sequence == current_sequence {
80 return Ok(());
81 }
82 } else if checkpoint_count(&transaction)? >= self.store.config().max_checkpoints {
83 return Err(SqliteSyncError::CapacityExceeded("checkpoint"));
84 }
85 transaction
86 .execute(
87 "INSERT INTO appcore_sync_checkpoint(peer_id, sequence, batch_hash)
88 VALUES (?1, ?2, ?3)
89 ON CONFLICT(peer_id) DO UPDATE SET
90 sequence = excluded.sequence, batch_hash = excluded.batch_hash",
91 params![peer_id, sequence, hash],
92 )
93 .map_err(SqliteSyncError::database)?;
94 transaction.commit().map_err(SqliteSyncError::database)
95 })
96 .map_err(|error| match error {
97 SqliteSyncError::CorruptRecord("checkpoint conflict") => {
98 SyncError::InvalidSequence(sequence_u64)
99 }
100 other => other.sync(),
101 })
102 }
103}
104
105pub(crate) fn validate_peer_id(peer_id: &str) -> SyncResult<()> {
106 if peer_id.is_empty()
107 || peer_id.len() > MAX_PEER_ID_BYTES
108 || !peer_id.chars().all(|character| {
109 character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | ':' | '-')
110 })
111 {
112 return Err(SyncError::InvalidPeerId);
113 }
114 Ok(())
115}
116
117pub(crate) fn validate_hash(hash: &str) -> SyncResult<()> {
118 if hash.is_empty() || (hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit())) {
119 Ok(())
120 } else {
121 Err(SyncError::ReplicationFailed(
122 "invalid checkpoint hash".to_string(),
123 ))
124 }
125}
126
127fn checkpoint_count(connection: &rusqlite::Connection) -> Result<usize, SqliteSyncError> {
128 let count: i64 = connection
129 .query_row("SELECT COUNT(*) FROM appcore_sync_checkpoint", [], |row| {
130 row.get(0)
131 })
132 .map_err(SqliteSyncError::database)?;
133 usize::try_from(count).map_err(|_| SqliteSyncError::CorruptRecord("checkpoint count"))
134}