1use crate::sync::codec::{bytes_to_hex, hex_to_bytes};
14use crate::sync::error::{SyncError, SyncResult};
15use crate::sync::persistence::{
16 acquire_persistence_lock, atomic_write, read_bounded_text, split_format,
17};
18use crate::sync::snapshot::{snapshot_from_records, validate_snapshot, ReplicationSnapshot};
19use sha2::{Digest, Sha256};
20use std::collections::HashMap;
21use std::fs;
22use std::io::Write;
23use std::path::{Path, PathBuf};
24
25pub const REPLICATION_LOG_FORMAT_V1: &str = "# appcore-replication-log-v1";
27const MAX_REPLICATION_LOG_BYTES: u64 = 256 * 1024 * 1024;
28const MAX_REPLICATION_RECORD_BYTES: usize = 1024 * 1024;
29
30pub trait ReplicationLog {
32 fn append(&mut self, record: Vec<u8>) -> SyncResult<usize>;
34 fn append_with_sequence(&mut self, record: Vec<u8>, sequence: u64) -> SyncResult<usize>;
36 fn event_at_sequence(&self, _sequence: u64) -> SyncResult<Option<Vec<u8>>> {
38 Ok(None)
39 }
40 fn events_since(&self, index: usize) -> SyncResult<Vec<Vec<u8>>>;
42 fn last_index(&self) -> usize;
44 fn len(&self) -> usize;
46 fn is_empty(&self) -> bool;
48 fn create_snapshot(&self) -> SyncResult<ReplicationSnapshot> {
50 Err(SyncError::SnapshotUnsupported)
51 }
52 fn restore_snapshot(&mut self, _snapshot: &ReplicationSnapshot) -> SyncResult<()> {
54 Err(SyncError::SnapshotUnsupported)
55 }
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct InMemoryReplicationLog {
61 events: Vec<ReplicationRecord>,
62 sequence_indices: HashMap<u64, usize>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub(super) struct ReplicationRecord {
67 pub(super) index: usize,
68 pub(super) sequence: u64,
69 pub(super) payload: Vec<u8>,
70 pub(super) previous_hash: String,
71 pub(super) record_hash: String,
72}
73
74impl InMemoryReplicationLog {
75 pub fn new() -> Self {
77 Self {
78 events: Vec::new(),
79 sequence_indices: HashMap::new(),
80 }
81 }
82
83 pub fn append_with_sequence(&mut self, record: Vec<u8>, sequence: u64) -> SyncResult<usize> {
85 validate_record_size(&record)?;
86 if sequence > 0 {
87 if let Some(existing) = self
88 .sequence_indices
89 .get(&sequence)
90 .and_then(|offset| self.events.get(*offset))
91 {
92 return if existing.payload == record {
93 Ok(existing.index)
94 } else {
95 Err(SyncError::SequenceConflict(sequence))
96 };
97 }
98 }
99 let index = self.events.len() + 1;
100 let previous_hash = self
101 .events
102 .last()
103 .map(|record| record.record_hash.clone())
104 .unwrap_or_default();
105 let record_hash = replication_record_hash(&previous_hash, sequence, &record);
106 self.events.push(ReplicationRecord {
107 index,
108 sequence,
109 payload: record,
110 previous_hash,
111 record_hash,
112 });
113 self.sequence_indices.insert(sequence, index - 1);
114 Ok(index)
115 }
116
117 pub fn last_index(&self) -> usize {
119 self.events.len()
120 }
121
122 pub fn contains_sequence(&self, sequence: u64) -> bool {
124 self.sequence_indices.contains_key(&sequence)
125 }
126}
127
128impl ReplicationLog for InMemoryReplicationLog {
129 fn append(&mut self, record: Vec<u8>) -> SyncResult<usize> {
130 self.append_with_sequence(record, 0)
131 }
132
133 fn append_with_sequence(&mut self, record: Vec<u8>, sequence: u64) -> SyncResult<usize> {
134 self.append_with_sequence(record, sequence)
135 }
136
137 fn event_at_sequence(&self, sequence: u64) -> SyncResult<Option<Vec<u8>>> {
138 Ok(self
139 .sequence_indices
140 .get(&sequence)
141 .filter(|_| sequence > 0)
142 .and_then(|offset| self.events.get(*offset))
143 .map(|event| event.payload.clone()))
144 }
145
146 fn events_since(&self, index: usize) -> SyncResult<Vec<Vec<u8>>> {
147 if index > self.events.len() {
148 return Err(SyncError::LogIndexOutOfBounds {
149 index,
150 len: self.events.len(),
151 });
152 }
153 Ok(self.events[index..]
154 .iter()
155 .map(|record| record.payload.clone())
156 .collect::<Vec<_>>())
157 }
158
159 fn len(&self) -> usize {
160 self.events.len()
161 }
162
163 fn last_index(&self) -> usize {
164 self.last_index()
165 }
166
167 fn is_empty(&self) -> bool {
168 self.events.is_empty()
169 }
170
171 fn create_snapshot(&self) -> SyncResult<ReplicationSnapshot> {
172 Ok(snapshot_from_records(&self.events))
173 }
174
175 fn restore_snapshot(&mut self, snapshot: &ReplicationSnapshot) -> SyncResult<()> {
176 let records = validate_snapshot(snapshot)?;
177 self.sequence_indices = sequence_indices(&records);
178 self.events = records;
179 Ok(())
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct FileReplicationLog {
186 file_path: PathBuf,
187 events: Vec<ReplicationRecord>,
188 sequence_indices: HashMap<u64, usize>,
189}
190
191impl FileReplicationLog {
192 pub fn new(storage_path: impl AsRef<Path>, relative_path: &str) -> SyncResult<Self> {
194 let storage_root = storage_path.as_ref();
195 let relative = PathBuf::from(relative_path);
196 if relative.as_os_str().is_empty()
197 || relative.is_absolute()
198 || relative
199 .components()
200 .any(|component| matches!(component, std::path::Component::ParentDir))
201 {
202 return Err(SyncError::ReplicationFailed(
203 "invalid replication log path".to_string(),
204 ));
205 }
206 let file_path = storage_root.join(&relative);
207 if let Some(parent) = file_path.parent() {
208 fs::create_dir_all(parent)
209 .map_err(|err| SyncError::ReplicationFailed(err.to_string()))?;
210 }
211 let _process_lock = acquire_persistence_lock(&file_path)?;
212 if !file_path.exists() {
213 atomic_write(
214 &file_path,
215 format!("{REPLICATION_LOG_FORMAT_V1}\n").as_bytes(),
216 )?;
217 }
218 let mut log = Self {
219 file_path,
220 events: Vec::new(),
221 sequence_indices: HashMap::new(),
222 };
223 log.reload_unlocked()?;
224 Ok(log)
225 }
226
227 pub fn file_path(&self) -> &Path {
229 &self.file_path
230 }
231
232 pub fn reload(&mut self) -> SyncResult<()> {
234 let _process_lock = acquire_persistence_lock(&self.file_path)?;
235 self.reload_unlocked()
236 }
237
238 fn reload_unlocked(&mut self) -> SyncResult<()> {
239 let text = read_bounded_text(&self.file_path, MAX_REPLICATION_LOG_BYTES)?;
240 let formatted = split_format(&text, REPLICATION_LOG_FORMAT_V1)?;
241 let (records, recovered_tail) = parse_replication_records(formatted.body)?;
242 if recovered_tail {
243 write_replication_records(&self.file_path, &records)?;
244 }
245 self.sequence_indices = sequence_indices(&records);
246 self.events = records;
247 Ok(())
248 }
249}
250
251impl ReplicationLog for FileReplicationLog {
252 fn append(&mut self, record: Vec<u8>) -> SyncResult<usize> {
253 self.append_with_sequence(record, 0)
254 }
255
256 fn append_with_sequence(&mut self, record: Vec<u8>, sequence: u64) -> SyncResult<usize> {
257 let _process_lock = acquire_persistence_lock(&self.file_path)?;
258 self.reload_unlocked()?;
259 validate_record_size(&record)?;
260 if sequence > 0 {
261 if let Some(existing) = self
262 .sequence_indices
263 .get(&sequence)
264 .and_then(|offset| self.events.get(*offset))
265 {
266 return if existing.payload == record {
267 Ok(existing.index)
268 } else {
269 Err(SyncError::SequenceConflict(sequence))
270 };
271 }
272 }
273 let previous_hash = self
274 .events
275 .last()
276 .map(|entry| entry.record_hash.clone())
277 .unwrap_or_default();
278 let record_hash = replication_record_hash(&previous_hash, sequence, &record);
279 let line = format!(
280 "{}\t{}\t{}\t{}\n",
281 sequence,
282 bytes_to_hex(&record),
283 previous_hash,
284 record_hash
285 );
286 let current_bytes = fs::metadata(&self.file_path)
287 .map_err(|err| SyncError::ReplicationFailed(err.to_string()))?
288 .len();
289 if current_bytes
290 .checked_add(line.len() as u64)
291 .is_none_or(|bytes| bytes > MAX_REPLICATION_LOG_BYTES)
292 {
293 return Err(SyncError::ReplicationFailed(
294 "replication log exceeds configured limit".to_string(),
295 ));
296 }
297 let mut file = fs::OpenOptions::new()
298 .append(true)
299 .open(&self.file_path)
300 .map_err(|err| SyncError::ReplicationFailed(err.to_string()))?;
301 file.write_all(line.as_bytes())
302 .map_err(|err| SyncError::ReplicationFailed(err.to_string()))?;
303 file.sync_data()
304 .map_err(|err| SyncError::ReplicationFailed(err.to_string()))?;
305 let index = self.events.len() + 1;
306 self.events.push(ReplicationRecord {
307 index,
308 sequence,
309 payload: record,
310 previous_hash,
311 record_hash,
312 });
313 self.sequence_indices.insert(sequence, index - 1);
314 Ok(index)
315 }
316
317 fn event_at_sequence(&self, sequence: u64) -> SyncResult<Option<Vec<u8>>> {
318 Ok(self
319 .sequence_indices
320 .get(&sequence)
321 .filter(|_| sequence > 0)
322 .and_then(|offset| self.events.get(*offset))
323 .map(|event| event.payload.clone()))
324 }
325
326 fn events_since(&self, index: usize) -> SyncResult<Vec<Vec<u8>>> {
327 if index > self.events.len() {
328 return Err(SyncError::LogIndexOutOfBounds {
329 index,
330 len: self.events.len(),
331 });
332 }
333 Ok(self.events[index..]
334 .iter()
335 .map(|record| record.payload.clone())
336 .collect::<Vec<_>>())
337 }
338
339 fn last_index(&self) -> usize {
340 self.events.len()
341 }
342
343 fn len(&self) -> usize {
344 self.events.len()
345 }
346
347 fn is_empty(&self) -> bool {
348 self.events.is_empty()
349 }
350
351 fn create_snapshot(&self) -> SyncResult<ReplicationSnapshot> {
352 Ok(snapshot_from_records(&self.events))
353 }
354
355 fn restore_snapshot(&mut self, snapshot: &ReplicationSnapshot) -> SyncResult<()> {
356 let _process_lock = acquire_persistence_lock(&self.file_path)?;
357 let records = validate_snapshot(snapshot)?;
358 write_replication_records(&self.file_path, &records)?;
359 self.sequence_indices = sequence_indices(&records);
360 self.events = records;
361 Ok(())
362 }
363}
364
365fn sequence_indices(records: &[ReplicationRecord]) -> HashMap<u64, usize> {
366 records
367 .iter()
368 .enumerate()
369 .map(|(offset, record)| (record.sequence, offset))
370 .collect()
371}
372
373fn parse_replication_records(body: &str) -> SyncResult<(Vec<ReplicationRecord>, bool)> {
374 let (complete, recovered_tail) = complete_record_prefix(body);
375 let mut records = Vec::<ReplicationRecord>::new();
376 let mut sequences = HashMap::<u64, usize>::new();
377 let mut chain_head = String::new();
378 for (line_number, line) in complete.lines().enumerate() {
379 if line.trim().is_empty() {
380 continue;
381 }
382 let parts = line.split('\t').collect::<Vec<_>>();
383 if parts.len() != 4 {
384 let reason = if line.contains('\t') {
385 "invalid field count"
386 } else {
387 "missing separator"
388 };
389 return Err(corrupt_log(line_number, reason));
390 }
391 let sequence = parts[0]
392 .parse::<u64>()
393 .map_err(|_| corrupt_log(line_number, "invalid sequence"))?;
394 let payload =
395 hex_to_bytes(parts[1]).map_err(|_| corrupt_log(line_number, "invalid event hex"))?;
396 validate_record_size(&payload)?;
397 let record_hash = replication_record_hash(&chain_head, sequence, &payload);
398 if parts[2] != chain_head || parts[3] != record_hash {
399 return Err(corrupt_log(line_number, "hash chain mismatch"));
400 }
401 if sequence > 0 {
402 if let Some(offset) = sequences.get(&sequence) {
403 if records[*offset].payload != payload {
404 return Err(SyncError::SequenceConflict(sequence));
405 }
406 return Err(corrupt_log(line_number, "duplicate sequence"));
407 }
408 }
409 let index = records.len() + 1;
410 records.push(ReplicationRecord {
411 index,
412 sequence,
413 payload,
414 previous_hash: chain_head,
415 record_hash: record_hash.clone(),
416 });
417 sequences.insert(sequence, index - 1);
418 chain_head = record_hash;
419 }
420 Ok((records, recovered_tail))
421}
422
423fn complete_record_prefix(body: &str) -> (&str, bool) {
424 if body.is_empty() || body.ends_with('\n') {
425 return (body, false);
426 }
427 match body.rfind('\n') {
428 Some(last_newline) => (&body[..=last_newline], true),
429 None => ("", true),
430 }
431}
432
433fn corrupt_log(line_number: usize, reason: &'static str) -> SyncError {
434 SyncError::CorruptReplicationLog {
435 line: line_number + 1,
436 reason,
437 }
438}
439
440fn write_replication_records(path: &Path, records: &[ReplicationRecord]) -> SyncResult<()> {
441 let mut output = format!("{REPLICATION_LOG_FORMAT_V1}\n");
442 let mut previous_hash = String::new();
443 for record in records {
444 validate_record_size(&record.payload)?;
445 let hash = replication_record_hash(&previous_hash, record.sequence, &record.payload);
446 output.push_str(&format!(
447 "{}\t{}\t{}\t{}\n",
448 record.sequence,
449 bytes_to_hex(&record.payload),
450 previous_hash,
451 hash
452 ));
453 if output.len() as u64 > MAX_REPLICATION_LOG_BYTES {
454 return Err(SyncError::ReplicationFailed(
455 "replication log exceeds configured limit".to_string(),
456 ));
457 }
458 previous_hash = hash;
459 }
460 atomic_write(path, output.as_bytes())
461}
462
463pub(super) fn validate_record_size(payload: &[u8]) -> SyncResult<()> {
464 if payload.len() > MAX_REPLICATION_RECORD_BYTES {
465 return Err(SyncError::ReplicationFailed(
466 "replication record exceeds size limit".to_string(),
467 ));
468 }
469 Ok(())
470}
471
472pub(super) fn replication_record_hash(
473 previous_hash: &str,
474 sequence: u64,
475 payload: &[u8],
476) -> String {
477 let mut hasher = Sha256::new();
478 hasher.update(REPLICATION_LOG_FORMAT_V1.as_bytes());
479 hasher.update((previous_hash.len() as u64).to_be_bytes());
480 hasher.update(previous_hash.as_bytes());
481 hasher.update(sequence.to_be_bytes());
482 hasher.update((payload.len() as u64).to_be_bytes());
483 hasher.update(payload);
484 bytes_to_hex(&hasher.finalize())
485}