1use std::fs::{self, File, OpenOptions};
2use std::io::{self, Read, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use powdb_storage::catalog::CATALOG_VERSION;
8use powdb_storage::create_data_dir_secure;
9use powdb_storage::wal::{WalRecord, WAL_FORMAT_VERSION};
10
11const SEGMENT_MAGIC: &[u8; 4] = b"PRUL";
12const FOOTER_MAGIC: &[u8; 4] = b"RULF";
13pub const RETAINED_SEGMENT_FORMAT_VERSION: u16 = 1;
14
15const RESERVED: u16 = 0;
16const HEADER_LEN: usize = 4 + 2 + 2 + 2 + 2 + 4 + 8 + 8 + 8 + 16;
17const UNIT_HEADER_LEN: usize = 4 + 4 + 8 + 1 + 8;
18const FOOTER_LEN: usize = 4 + 4;
19const MAX_RETAINED_UNIT_DATA_SIZE: usize = 256 * 1024 * 1024;
20const MAX_RETAINED_SEGMENT_FILE_SIZE: u64 = 1024 * 1024 * 1024;
21static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct SegmentIdentity {
26 pub database_id: [u8; 16],
27 pub primary_generation: u64,
28 pub wal_format_version: u16,
29 pub catalog_version: u16,
30}
31
32impl SegmentIdentity {
33 pub fn current(database_id: [u8; 16], primary_generation: u64) -> Self {
41 Self::with_catalog_version(database_id, primary_generation, CATALOG_VERSION)
42 }
43
44 pub fn with_catalog_version(
52 database_id: [u8; 16],
53 primary_generation: u64,
54 catalog_version: u16,
55 ) -> Self {
56 Self {
57 database_id,
58 primary_generation,
59 wal_format_version: WAL_FORMAT_VERSION,
60 catalog_version,
61 }
62 }
63
64 pub fn lineage_matches(self, other: SegmentIdentity) -> bool {
69 self.database_id == other.database_id
70 && self.primary_generation == other.primary_generation
71 && self.wal_format_version == other.wal_format_version
72 }
73
74 pub fn validate(self) -> io::Result<()> {
75 validate_identity(self)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct RetainedUnit {
86 pub tx_id: u64,
87 pub record_type: u8,
88 pub lsn: u64,
89 pub data: Vec<u8>,
90}
91
92impl From<&WalRecord> for RetainedUnit {
93 fn from(record: &WalRecord) -> Self {
94 Self {
95 tx_id: record.tx_id,
96 record_type: record.record_type as u8,
97 lsn: record.lsn,
98 data: record.data.clone(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct RetainedSegment {
106 pub identity: SegmentIdentity,
107 pub start_lsn: u64,
108 pub end_lsn: u64,
109 pub units: Vec<RetainedUnit>,
110}
111
112impl RetainedSegment {
113 pub fn new(identity: SegmentIdentity, units: Vec<RetainedUnit>) -> io::Result<Self> {
114 validate_identity(identity)?;
115 validate_units(&units)?;
116 let start_lsn = units[0].lsn;
117 let end_lsn = units[units.len() - 1].lsn;
118 Ok(Self {
119 identity,
120 start_lsn,
121 end_lsn,
122 units,
123 })
124 }
125
126 pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
127 self.validate()?;
128
129 let mut out = Vec::with_capacity(HEADER_LEN + FOOTER_LEN);
130 out.extend_from_slice(SEGMENT_MAGIC);
131 out.extend_from_slice(&RETAINED_SEGMENT_FORMAT_VERSION.to_le_bytes());
132 out.extend_from_slice(&RESERVED.to_le_bytes());
133 out.extend_from_slice(&self.identity.wal_format_version.to_le_bytes());
134 out.extend_from_slice(&self.identity.catalog_version.to_le_bytes());
135 out.extend_from_slice(&checked_unit_count(self.units.len())?.to_le_bytes());
136 out.extend_from_slice(&self.start_lsn.to_le_bytes());
137 out.extend_from_slice(&self.end_lsn.to_le_bytes());
138 out.extend_from_slice(&self.identity.primary_generation.to_le_bytes());
139 out.extend_from_slice(&self.identity.database_id);
140
141 for unit in &self.units {
142 let data_len_u32 = checked_data_len(unit.data.len())?;
143 let total_len = (UNIT_HEADER_LEN as u32)
144 .checked_add(data_len_u32)
145 .ok_or_else(|| invalid_input("retained unit is too large"))?;
146 let crc = unit_crc(unit);
147 out.extend_from_slice(&total_len.to_le_bytes());
148 out.extend_from_slice(&crc.to_le_bytes());
149 out.extend_from_slice(&unit.tx_id.to_le_bytes());
150 out.push(unit.record_type);
151 out.extend_from_slice(&unit.lsn.to_le_bytes());
152 out.extend_from_slice(&unit.data);
153 }
154
155 let file_crc = crc32fast::hash(&out);
156 out.extend_from_slice(FOOTER_MAGIC);
157 out.extend_from_slice(&file_crc.to_le_bytes());
158 Ok(out)
159 }
160
161 pub fn validate(&self) -> io::Result<()> {
162 validate_identity(self.identity)?;
163 validate_units(&self.units)?;
164 if self.start_lsn != self.units[0].lsn {
165 return Err(invalid_input(
166 "retained segment start LSN does not match first unit",
167 ));
168 }
169 if self.end_lsn != self.units[self.units.len() - 1].lsn {
170 return Err(invalid_input(
171 "retained segment end LSN does not match last unit",
172 ));
173 }
174 Ok(())
175 }
176
177 pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
178 if bytes.len() < HEADER_LEN + FOOTER_LEN {
179 return Err(invalid_data("retained segment is truncated"));
180 }
181
182 let footer_start = bytes.len() - FOOTER_LEN;
183 if &bytes[footer_start..footer_start + 4] != FOOTER_MAGIC {
184 return Err(invalid_data("retained segment footer magic mismatch"));
185 }
186 let expected_file_crc = read_u32(bytes, footer_start + 4, "segment footer crc")?;
187 let actual_file_crc = crc32fast::hash(&bytes[..footer_start]);
188 if expected_file_crc != actual_file_crc {
189 return Err(invalid_data("retained segment footer CRC mismatch"));
190 }
191
192 let mut pos = 0usize;
193 if read_exact(bytes, &mut pos, 4, "segment magic")? != SEGMENT_MAGIC {
194 return Err(invalid_data("retained segment magic mismatch"));
195 }
196 let segment_version = read_u16_at_cursor(bytes, &mut pos, "segment version")?;
197 if segment_version != RETAINED_SEGMENT_FORMAT_VERSION {
198 return Err(invalid_data("unsupported retained segment format version"));
199 }
200 let reserved = read_u16_at_cursor(bytes, &mut pos, "reserved")?;
201 if reserved != RESERVED {
202 return Err(invalid_data("retained segment reserved field must be zero"));
203 }
204 let wal_format_version = read_u16_at_cursor(bytes, &mut pos, "WAL format version")?;
205 let catalog_version = read_u16_at_cursor(bytes, &mut pos, "catalog version")?;
206 let record_count = read_u32_at_cursor(bytes, &mut pos, "record count")? as usize;
207 let start_lsn = read_u64_at_cursor(bytes, &mut pos, "start LSN")?;
208 let end_lsn = read_u64_at_cursor(bytes, &mut pos, "end LSN")?;
209 let primary_generation = read_u64_at_cursor(bytes, &mut pos, "primary generation")?;
210 let database_id_slice = read_exact(bytes, &mut pos, 16, "database id")?;
211 let mut database_id = [0u8; 16];
212 database_id.copy_from_slice(database_id_slice);
213
214 let identity = SegmentIdentity {
215 database_id,
216 primary_generation,
217 wal_format_version,
218 catalog_version,
219 };
220 validate_identity(identity)?;
221
222 if record_count == 0 {
223 return Err(invalid_data(
224 "retained segment must contain at least one unit",
225 ));
226 }
227 let max_possible_units = footer_start.saturating_sub(pos) / UNIT_HEADER_LEN;
228 if record_count > max_possible_units {
229 return Err(invalid_data(
230 "retained segment record count exceeds file capacity",
231 ));
232 }
233
234 let mut units = Vec::new();
235 units
236 .try_reserve(record_count)
237 .map_err(|_| invalid_data("retained segment record count is too large"))?;
238 for _ in 0..record_count {
239 if pos >= footer_start {
240 return Err(invalid_data("retained segment record count exceeds file"));
241 }
242 let total_len = read_u32(bytes, pos, "unit total length")? as usize;
243 if total_len < UNIT_HEADER_LEN {
244 return Err(invalid_data("retained unit length is smaller than header"));
245 }
246 if pos
247 .checked_add(total_len)
248 .is_none_or(|end| end > footer_start)
249 {
250 return Err(invalid_data("retained unit extends beyond segment"));
251 }
252 let unit_start = pos;
253 pos += 4;
254 let stored_crc = read_u32_at_cursor(bytes, &mut pos, "unit crc")?;
255 let tx_id = read_u64_at_cursor(bytes, &mut pos, "unit tx id")?;
256 let record_type = read_u8_at_cursor(bytes, &mut pos, "unit record type")?;
257 let lsn = read_u64_at_cursor(bytes, &mut pos, "unit LSN")?;
258 let data_len = total_len - UNIT_HEADER_LEN;
259 if data_len > MAX_RETAINED_UNIT_DATA_SIZE {
260 return Err(invalid_data("retained unit payload exceeds maximum size"));
261 }
262 let data = read_exact(bytes, &mut pos, data_len, "unit payload")?.to_vec();
263 debug_assert_eq!(pos, unit_start + total_len);
264
265 let unit = RetainedUnit {
266 tx_id,
267 record_type,
268 lsn,
269 data,
270 };
271 if !is_known_record_type(unit.record_type) {
272 return Err(invalid_data("unknown retained unit record type"));
273 }
274 if stored_crc != unit_crc(&unit) {
275 return Err(invalid_data("retained unit CRC mismatch"));
276 }
277 units.push(unit);
278 }
279
280 if pos != footer_start {
281 return Err(invalid_data("retained segment has trailing bytes"));
282 }
283
284 let segment = Self::new(identity, units)?;
285 if segment.start_lsn != start_lsn || segment.end_lsn != end_lsn {
286 return Err(invalid_data(
287 "retained segment LSN range does not match records",
288 ));
289 }
290 Ok(segment)
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct SegmentFile {
296 pub path: PathBuf,
297 pub start_lsn: u64,
298 pub end_lsn: u64,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct RetainedTailAvailability {
303 pub since_lsn: u64,
304 pub through_lsn: u64,
305 pub units_available: usize,
306 pub first_lsn: Option<u64>,
307 pub last_lsn: Option<u64>,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct RetainedTailProgress {
312 pub since_lsn: u64,
313 pub requested_through_lsn: u64,
314 pub contiguous_through_lsn: u64,
315 pub units_available: usize,
316 pub first_lsn: Option<u64>,
317 pub last_lsn: Option<u64>,
318}
319
320pub fn segment_file_name(start_lsn: u64, end_lsn: u64) -> String {
321 format!("retained-{start_lsn:020}-{end_lsn:020}.prul")
322}
323
324pub fn write_segment_atomic(dir: &Path, segment: &RetainedSegment) -> io::Result<PathBuf> {
325 create_data_dir_secure(dir)?;
326 let final_path = dir.join(segment_file_name(segment.start_lsn, segment.end_lsn));
327
328 let temp_path = temp_segment_path(dir, segment);
329 let bytes = segment.to_bytes()?;
330 let write_result: io::Result<()> = (|| {
331 let mut file = OpenOptions::new()
332 .write(true)
333 .create_new(true)
334 .open(&temp_path)?;
335 file.write_all(&bytes)?;
336 file.sync_all()?;
337 drop(file);
338
339 fs::hard_link(&temp_path, &final_path)?;
342 fsync_dir(dir)?;
343 let _ = fs::remove_file(&temp_path);
344 let _ = fsync_dir(dir);
345 Ok(())
346 })();
347 if write_result.is_err() {
348 let _ = fs::remove_file(&temp_path);
349 }
350 write_result?;
351 Ok(final_path)
352}
353
354pub fn read_segment_file(path: &Path) -> io::Result<RetainedSegment> {
355 let file = File::open(path)?;
356 let len = file.metadata()?.len();
357 if len > MAX_RETAINED_SEGMENT_FILE_SIZE {
358 return Err(invalid_data("retained segment file exceeds maximum size"));
359 }
360 let capacity = usize::try_from(len)
361 .map_err(|_| invalid_data("retained segment file size exceeds platform capacity"))?;
362 let mut bytes = Vec::new();
363 bytes
364 .try_reserve_exact(capacity)
365 .map_err(|_| invalid_data("retained segment file is too large to allocate"))?;
366 let mut limited = file.take(MAX_RETAINED_SEGMENT_FILE_SIZE + 1);
367 limited.read_to_end(&mut bytes)?;
368 if bytes.len() as u64 > MAX_RETAINED_SEGMENT_FILE_SIZE {
369 return Err(invalid_data("retained segment file exceeds maximum size"));
370 }
371 RetainedSegment::from_bytes(&bytes)
372}
373
374pub fn list_segment_files(dir: &Path) -> io::Result<Vec<SegmentFile>> {
375 let mut files = Vec::new();
376 let entries = match fs::read_dir(dir) {
377 Ok(entries) => entries,
378 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(files),
379 Err(err) => return Err(err),
380 };
381 for entry in entries {
382 let entry = entry?;
383 let path = entry.path();
384 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
385 continue;
386 };
387 if let Some((start_lsn, end_lsn)) = parse_segment_file_name(name) {
388 files.push(SegmentFile {
389 path,
390 start_lsn,
391 end_lsn,
392 });
393 }
394 }
395 files.sort_by_key(|file| (file.start_lsn, file.end_lsn));
396 Ok(files)
397}
398
399pub fn read_units_since(
400 dir: &Path,
401 expected_identity: SegmentIdentity,
402 since_lsn: u64,
403 max_units: usize,
404) -> io::Result<Vec<RetainedUnit>> {
405 read_units_through(dir, expected_identity, since_lsn, u64::MAX, max_units)
406}
407
408pub fn read_units_through(
409 dir: &Path,
410 expected_identity: SegmentIdentity,
411 since_lsn: u64,
412 through_lsn: u64,
413 max_units: usize,
414) -> io::Result<Vec<RetainedUnit>> {
415 validate_identity(expected_identity)?;
416 if max_units == 0 || through_lsn <= since_lsn {
417 return Ok(Vec::new());
418 }
419
420 let mut out = Vec::new();
421 let Some(mut expected_next_lsn) = since_lsn.checked_add(1) else {
422 return Ok(out);
423 };
424 let mut have_started = false;
425 let mut chain_catalog_version = 0u16;
426
427 for file in list_segment_files(dir)? {
428 if file.start_lsn > through_lsn {
429 if expected_next_lsn <= through_lsn {
430 return Err(invalid_data(format!(
431 "retained segment gap: expected LSN {}, found {}",
432 expected_next_lsn, file.start_lsn
433 )));
434 }
435 break;
436 }
437 let segment = read_segment_file(&file.path)?;
438 if segment.start_lsn != file.start_lsn || segment.end_lsn != file.end_lsn {
439 return Err(invalid_data(format!(
440 "retained segment filename range {}-{} does not match header range {}-{}",
441 file.start_lsn, file.end_lsn, segment.start_lsn, segment.end_lsn
442 )));
443 }
444 if !segment.identity.lineage_matches(expected_identity) {
445 return Err(invalid_data(
446 "retained segment identity does not match expected database history",
447 ));
448 }
449 if segment.identity.catalog_version < chain_catalog_version {
450 return Err(invalid_data(format!(
451 "retained segment catalog format decreased across chain (v{} then v{}); rebootstrap required",
452 chain_catalog_version, segment.identity.catalog_version
453 )));
454 }
455 chain_catalog_version = segment.identity.catalog_version;
456
457 if segment.end_lsn <= since_lsn {
458 continue;
459 }
460
461 if !have_started {
462 if segment.start_lsn > expected_next_lsn {
463 return Err(invalid_data(format!(
464 "retained segment gap: expected LSN {}, found {}",
465 expected_next_lsn, segment.start_lsn
466 )));
467 }
468 } else if segment.start_lsn < expected_next_lsn {
469 return Err(invalid_data(format!(
470 "retained segment overlap: expected LSN {}, found {}",
471 expected_next_lsn, segment.start_lsn
472 )));
473 } else if segment.start_lsn > expected_next_lsn {
474 return Err(invalid_data(format!(
475 "retained segment gap: expected LSN {}, found {}",
476 expected_next_lsn, segment.start_lsn
477 )));
478 }
479
480 for unit in segment.units {
481 if unit.lsn < expected_next_lsn {
482 if have_started {
483 return Err(invalid_data(format!(
484 "retained unit overlap: expected LSN {}, found {}",
485 expected_next_lsn, unit.lsn
486 )));
487 }
488 continue;
489 }
490 if unit.lsn > expected_next_lsn {
491 return Err(invalid_data(format!(
492 "retained unit gap: expected LSN {}, found {}",
493 expected_next_lsn, unit.lsn
494 )));
495 }
496 if unit.lsn > through_lsn {
497 return Ok(out);
498 }
499 out.push(unit);
500 have_started = true;
501 expected_next_lsn = expected_next_lsn
502 .checked_add(1)
503 .ok_or_else(|| invalid_data("retained unit LSN overflow"))?;
504 if out.len() == max_units {
505 return Ok(out);
506 }
507 }
508 }
509 Ok(out)
510}
511
512pub fn validate_retained_tail_available(
513 dir: &Path,
514 expected_identity: SegmentIdentity,
515 since_lsn: u64,
516 through_lsn: u64,
517) -> io::Result<RetainedTailAvailability> {
518 validate_identity(expected_identity)?;
519 if through_lsn <= since_lsn {
520 return Ok(RetainedTailAvailability {
521 since_lsn,
522 through_lsn,
523 units_available: 0,
524 first_lsn: None,
525 last_lsn: None,
526 });
527 }
528
529 let mut expected_next_lsn = since_lsn
530 .checked_add(1)
531 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
532 let mut units_available = 0usize;
533 let mut first_lsn = None;
534 let mut have_started = false;
535 let mut chain_catalog_version = 0u16;
536
537 for file in list_segment_files(dir)? {
538 let segment = read_segment_file(&file.path)?;
539 if segment.start_lsn != file.start_lsn || segment.end_lsn != file.end_lsn {
540 return Err(invalid_data(format!(
541 "retained segment filename range {}-{} does not match header range {}-{}",
542 file.start_lsn, file.end_lsn, segment.start_lsn, segment.end_lsn
543 )));
544 }
545 if !segment.identity.lineage_matches(expected_identity) {
546 return Err(invalid_data(
547 "retained segment identity does not match expected database history",
548 ));
549 }
550 if segment.identity.catalog_version < chain_catalog_version {
551 return Err(invalid_data(format!(
552 "retained segment catalog format decreased across chain (v{} then v{}); rebootstrap required",
553 chain_catalog_version, segment.identity.catalog_version
554 )));
555 }
556 chain_catalog_version = segment.identity.catalog_version;
557
558 if segment.end_lsn < expected_next_lsn {
559 continue;
560 }
561 if !have_started {
562 if segment.start_lsn > expected_next_lsn {
563 return Err(invalid_data(format!(
564 "retained segment gap: expected LSN {}, found {}",
565 expected_next_lsn, segment.start_lsn
566 )));
567 }
568 } else if segment.start_lsn < expected_next_lsn {
569 return Err(invalid_data(format!(
570 "retained segment overlap: expected LSN {}, found {}",
571 expected_next_lsn, segment.start_lsn
572 )));
573 } else if segment.start_lsn > expected_next_lsn {
574 return Err(invalid_data(format!(
575 "retained segment gap: expected LSN {}, found {}",
576 expected_next_lsn, segment.start_lsn
577 )));
578 }
579
580 for unit in segment.units {
581 if unit.lsn < expected_next_lsn {
582 continue;
583 }
584 if unit.lsn > expected_next_lsn {
585 return Err(invalid_data(format!(
586 "retained unit gap: expected LSN {}, found {}",
587 expected_next_lsn, unit.lsn
588 )));
589 }
590 first_lsn = Some(first_lsn.unwrap_or(unit.lsn));
591 have_started = true;
592 units_available = units_available
593 .checked_add(1)
594 .ok_or_else(|| invalid_data("retained tail unit count overflow"))?;
595 if unit.lsn >= through_lsn {
596 return Ok(RetainedTailAvailability {
597 since_lsn,
598 through_lsn,
599 units_available,
600 first_lsn,
601 last_lsn: Some(unit.lsn),
602 });
603 }
604 expected_next_lsn = expected_next_lsn
605 .checked_add(1)
606 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
607 }
608 }
609
610 Err(invalid_data(format!(
611 "retained tail missing required LSN {} through {}",
612 expected_next_lsn, through_lsn
613 )))
614}
615
616pub fn retained_tail_progress(
617 dir: &Path,
618 expected_identity: SegmentIdentity,
619 since_lsn: u64,
620 requested_through_lsn: u64,
621) -> io::Result<RetainedTailProgress> {
622 validate_identity(expected_identity)?;
623 if requested_through_lsn <= since_lsn {
624 return Ok(RetainedTailProgress {
625 since_lsn,
626 requested_through_lsn,
627 contiguous_through_lsn: since_lsn,
628 units_available: 0,
629 first_lsn: None,
630 last_lsn: None,
631 });
632 }
633
634 let mut expected_next_lsn = since_lsn
635 .checked_add(1)
636 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
637 let mut units_available = 0usize;
638 let mut first_lsn = None;
639 let mut last_lsn = None;
640 let mut have_started = false;
641 let mut chain_catalog_version = 0u16;
642
643 for file in list_segment_files(dir)? {
644 if file.end_lsn <= since_lsn {
645 continue;
646 }
647 if file.start_lsn > requested_through_lsn {
648 break;
649 }
650
651 let segment = read_segment_file(&file.path)?;
652 if segment.start_lsn != file.start_lsn || segment.end_lsn != file.end_lsn {
653 return Err(invalid_data(format!(
654 "retained segment filename range {}-{} does not match header range {}-{}",
655 file.start_lsn, file.end_lsn, segment.start_lsn, segment.end_lsn
656 )));
657 }
658 if !segment.identity.lineage_matches(expected_identity) {
659 return Err(invalid_data(
660 "retained segment identity does not match expected database history",
661 ));
662 }
663 if segment.identity.catalog_version < chain_catalog_version {
664 return Err(invalid_data(format!(
665 "retained segment catalog format decreased across chain (v{} then v{}); rebootstrap required",
666 chain_catalog_version, segment.identity.catalog_version
667 )));
668 }
669 chain_catalog_version = segment.identity.catalog_version;
670 if segment.end_lsn < expected_next_lsn {
671 continue;
672 }
673
674 if !have_started {
675 if segment.start_lsn > expected_next_lsn {
676 return Err(invalid_data(format!(
677 "retained segment gap: expected LSN {}, found {}",
678 expected_next_lsn, segment.start_lsn
679 )));
680 }
681 } else if segment.start_lsn < expected_next_lsn {
682 return Err(invalid_data(format!(
683 "retained segment overlap: expected LSN {}, found {}",
684 expected_next_lsn, segment.start_lsn
685 )));
686 } else if segment.start_lsn > expected_next_lsn {
687 return Err(invalid_data(format!(
688 "retained segment gap: expected LSN {}, found {}",
689 expected_next_lsn, segment.start_lsn
690 )));
691 }
692
693 for unit in segment.units {
694 if unit.lsn < expected_next_lsn {
695 continue;
696 }
697 if unit.lsn > expected_next_lsn {
698 return Err(invalid_data(format!(
699 "retained unit gap: expected LSN {}, found {}",
700 expected_next_lsn, unit.lsn
701 )));
702 }
703 if unit.lsn > requested_through_lsn {
704 return Ok(RetainedTailProgress {
705 since_lsn,
706 requested_through_lsn,
707 contiguous_through_lsn: last_lsn.unwrap_or(since_lsn),
708 units_available,
709 first_lsn,
710 last_lsn,
711 });
712 }
713
714 first_lsn = Some(first_lsn.unwrap_or(unit.lsn));
715 last_lsn = Some(unit.lsn);
716 have_started = true;
717 units_available = units_available
718 .checked_add(1)
719 .ok_or_else(|| invalid_data("retained tail unit count overflow"))?;
720 expected_next_lsn = expected_next_lsn
721 .checked_add(1)
722 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
723 if unit.lsn >= requested_through_lsn {
724 return Ok(RetainedTailProgress {
725 since_lsn,
726 requested_through_lsn,
727 contiguous_through_lsn: unit.lsn,
728 units_available,
729 first_lsn,
730 last_lsn,
731 });
732 }
733 }
734 }
735
736 Ok(RetainedTailProgress {
737 since_lsn,
738 requested_through_lsn,
739 contiguous_through_lsn: last_lsn.unwrap_or(since_lsn),
740 units_available,
741 first_lsn,
742 last_lsn,
743 })
744}
745
746fn parse_segment_file_name(name: &str) -> Option<(u64, u64)> {
747 let rest = name.strip_prefix("retained-")?;
748 let rest = rest.strip_suffix(".prul")?;
749 let (start, end) = rest.split_once('-')?;
750 let start_lsn = start.parse().ok()?;
751 let end_lsn = end.parse().ok()?;
752 Some((start_lsn, end_lsn))
753}
754
755fn temp_segment_path(dir: &Path, segment: &RetainedSegment) -> PathBuf {
756 let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
757 let nanos = SystemTime::now()
758 .duration_since(UNIX_EPOCH)
759 .map(|duration| duration.as_nanos())
760 .unwrap_or(0);
761 dir.join(format!(
762 ".{}.tmp.{}.{}.{}",
763 segment_file_name(segment.start_lsn, segment.end_lsn),
764 std::process::id(),
765 nanos,
766 counter
767 ))
768}
769
770fn validate_identity(identity: SegmentIdentity) -> io::Result<()> {
771 if identity.primary_generation == 0 {
772 return Err(invalid_data(
773 "retained segment primary generation must be non-zero",
774 ));
775 }
776 if identity.database_id == [0; 16] {
777 return Err(invalid_data(
778 "retained segment database id must be non-zero",
779 ));
780 }
781 if identity.wal_format_version != WAL_FORMAT_VERSION {
782 return Err(invalid_data("retained segment WAL format is unsupported"));
783 }
784 if identity.catalog_version == 0 {
790 return Err(invalid_data(
791 "retained segment catalog format v0 is invalid",
792 ));
793 }
794 if identity.catalog_version > CATALOG_VERSION {
795 return Err(invalid_data(format!(
796 "retained segment catalog format v{} is newer than this binary supports (max v{})",
797 identity.catalog_version, CATALOG_VERSION
798 )));
799 }
800 Ok(())
801}
802
803fn validate_units(units: &[RetainedUnit]) -> io::Result<()> {
804 if units.is_empty() {
805 return Err(invalid_input(
806 "retained segment must contain at least one unit",
807 ));
808 }
809 if units.len() > u32::MAX as usize {
810 return Err(invalid_input("retained segment has too many units"));
811 }
812
813 let mut expected_lsn = units[0].lsn;
814 if expected_lsn == 0 {
815 return Err(invalid_input("retained unit LSN must be non-zero"));
816 }
817 for unit in units {
818 if unit.lsn != expected_lsn {
819 return Err(invalid_input("retained unit LSNs must be contiguous"));
820 }
821 if !is_known_record_type(unit.record_type) {
822 return Err(invalid_input("unknown retained unit record type"));
823 }
824 checked_data_len(unit.data.len())?;
825 expected_lsn = expected_lsn
826 .checked_add(1)
827 .ok_or_else(|| invalid_input("retained unit LSN overflow"))?;
828 }
829 Ok(())
830}
831
832fn is_known_record_type(record_type: u8) -> bool {
833 matches!(record_type, 1..=10)
834}
835
836fn unit_crc(unit: &RetainedUnit) -> u32 {
837 let mut crc_input = Vec::with_capacity(17 + unit.data.len());
838 crc_input.extend_from_slice(&unit.tx_id.to_le_bytes());
839 crc_input.push(unit.record_type);
840 crc_input.extend_from_slice(&unit.lsn.to_le_bytes());
841 crc_input.extend_from_slice(&unit.data);
842 crc32fast::hash(&crc_input)
843}
844
845fn checked_unit_count(count: usize) -> io::Result<u32> {
846 u32::try_from(count).map_err(|_| invalid_input("retained segment has too many units"))
847}
848
849fn checked_data_len(len: usize) -> io::Result<u32> {
850 if len > MAX_RETAINED_UNIT_DATA_SIZE {
851 return Err(invalid_input("retained unit payload exceeds maximum size"));
852 }
853 u32::try_from(len).map_err(|_| invalid_input("retained unit payload is too large"))
854}
855
856fn read_exact<'a>(
857 bytes: &'a [u8],
858 pos: &mut usize,
859 len: usize,
860 field: &str,
861) -> io::Result<&'a [u8]> {
862 let end = pos
863 .checked_add(len)
864 .ok_or_else(|| invalid_data(format!("{field} offset overflow")))?;
865 if end > bytes.len() {
866 return Err(invalid_data(format!("truncated {field}")));
867 }
868 let slice = &bytes[*pos..end];
869 *pos = end;
870 Ok(slice)
871}
872
873fn read_u8_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u8> {
874 let slice = read_exact(bytes, pos, 1, field)?;
875 Ok(slice[0])
876}
877
878fn read_u16_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u16> {
879 let slice = read_exact(bytes, pos, 2, field)?;
880 let arr: [u8; 2] = slice
881 .try_into()
882 .map_err(|_| invalid_data(format!("invalid {field}")))?;
883 Ok(u16::from_le_bytes(arr))
884}
885
886fn read_u32_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u32> {
887 let slice = read_exact(bytes, pos, 4, field)?;
888 let arr: [u8; 4] = slice
889 .try_into()
890 .map_err(|_| invalid_data(format!("invalid {field}")))?;
891 Ok(u32::from_le_bytes(arr))
892}
893
894fn read_u64_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u64> {
895 let slice = read_exact(bytes, pos, 8, field)?;
896 let arr: [u8; 8] = slice
897 .try_into()
898 .map_err(|_| invalid_data(format!("invalid {field}")))?;
899 Ok(u64::from_le_bytes(arr))
900}
901
902fn read_u32(bytes: &[u8], pos: usize, field: &str) -> io::Result<u32> {
903 let end = pos
904 .checked_add(4)
905 .ok_or_else(|| invalid_data(format!("{field} offset overflow")))?;
906 if end > bytes.len() {
907 return Err(invalid_data(format!("truncated {field}")));
908 }
909 let arr: [u8; 4] = bytes[pos..end]
910 .try_into()
911 .map_err(|_| invalid_data(format!("invalid {field}")))?;
912 Ok(u32::from_le_bytes(arr))
913}
914
915#[cfg(unix)]
916fn fsync_dir(dir: &Path) -> io::Result<()> {
917 File::open(dir)?.sync_all()
918}
919
920#[cfg(not(unix))]
921fn fsync_dir(_dir: &Path) -> io::Result<()> {
922 Ok(())
923}
924
925fn invalid_input(message: impl Into<String>) -> io::Error {
926 io::Error::new(io::ErrorKind::InvalidInput, message.into())
927}
928
929fn invalid_data(message: impl Into<String>) -> io::Error {
930 io::Error::new(io::ErrorKind::InvalidData, message.into())
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936 use powdb_storage::catalog::LEGACY_CATALOG_VERSION;
937 use std::sync::{Arc, Barrier};
938
939 fn identity() -> SegmentIdentity {
940 SegmentIdentity::current(*b"0123456789abcdef", 7)
941 }
942
943 fn unit(lsn: u64) -> RetainedUnit {
944 RetainedUnit {
945 tx_id: 42,
946 record_type: 1,
947 lsn,
948 data: format!("payload-{lsn}").into_bytes(),
949 }
950 }
951
952 #[test]
953 fn segment_round_trips() {
954 let segment = RetainedSegment::new(identity(), vec![unit(10), unit(11)]).unwrap();
955 let bytes = segment.to_bytes().unwrap();
956 let decoded = RetainedSegment::from_bytes(&bytes).unwrap();
957 assert_eq!(decoded, segment);
958 }
959
960 #[test]
961 fn rejects_empty_and_non_contiguous_segments() {
962 assert!(RetainedSegment::new(identity(), Vec::new()).is_err());
963 let err = RetainedSegment::new(identity(), vec![unit(10), unit(12)]).unwrap_err();
964 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
965 }
966
967 #[test]
968 fn public_segment_literal_is_validated_before_serialization() {
969 let invalid = RetainedSegment {
970 identity: identity(),
971 start_lsn: 99,
972 end_lsn: 100,
973 units: vec![unit(1), unit(2)],
974 };
975
976 let err = invalid.to_bytes().unwrap_err();
977 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
978
979 let dir = tempfile::tempdir().unwrap();
980 let err = write_segment_atomic(dir.path(), &invalid).unwrap_err();
981 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
982 assert!(
983 list_segment_files(dir.path()).unwrap().is_empty(),
984 "invalid public segment literals must not publish unreadable retained logs"
985 );
986 }
987
988 #[test]
989 fn rejects_corrupt_footer_checksum() {
990 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
991 let mut bytes = segment.to_bytes().unwrap();
992 bytes[HEADER_LEN + 3] ^= 0x55;
993 let err = RetainedSegment::from_bytes(&bytes).unwrap_err();
994 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
995 }
996
997 #[test]
998 fn rejects_truncated_segment() {
999 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
1000 let bytes = segment.to_bytes().unwrap();
1001 let truncated = &bytes[..bytes.len() - 3];
1002 let err = RetainedSegment::from_bytes(truncated).unwrap_err();
1003 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1004 }
1005
1006 #[test]
1007 fn atomic_publish_and_range_read_work() {
1008 let dir = tempfile::tempdir().unwrap();
1009 let first = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
1010 let second = RetainedSegment::new(identity(), vec![unit(3), unit(4)]).unwrap();
1011
1012 let first_path = write_segment_atomic(dir.path(), &first).unwrap();
1013 let second_path = write_segment_atomic(dir.path(), &second).unwrap();
1014 assert!(first_path.exists());
1015 assert!(second_path.exists());
1016
1017 let files = list_segment_files(dir.path()).unwrap();
1018 assert_eq!(files.len(), 2);
1019 assert_eq!(files[0].start_lsn, 1);
1020 assert_eq!(files[1].start_lsn, 3);
1021
1022 let units = read_units_since(dir.path(), identity(), 2, 10).unwrap();
1023 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
1024 assert_eq!(lsns, vec![3, 4]);
1025 }
1026
1027 #[cfg(unix)]
1028 #[test]
1029 fn atomic_publish_creates_owner_only_segment_dir() {
1030 use std::os::unix::fs::PermissionsExt;
1031
1032 let dir = tempfile::tempdir().unwrap();
1033 let segment_dir = dir.path().join("retained");
1034 let segment = RetainedSegment::new(identity(), vec![unit(1)]).unwrap();
1035
1036 write_segment_atomic(&segment_dir, &segment).unwrap();
1037
1038 let mode = std::fs::metadata(&segment_dir)
1039 .unwrap()
1040 .permissions()
1041 .mode()
1042 & 0o777;
1043 assert_eq!(mode, 0o700);
1044 }
1045
1046 #[test]
1047 fn read_segment_file_rejects_oversized_file_before_reading() {
1048 let dir = tempfile::tempdir().unwrap();
1049 let path = dir.path().join(segment_file_name(1, 1));
1050 let file = File::create(&path).unwrap();
1051 file.set_len(MAX_RETAINED_SEGMENT_FILE_SIZE + 1).unwrap();
1052 drop(file);
1053
1054 let err = read_segment_file(&path).unwrap_err();
1055 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1056 assert!(err.to_string().contains("maximum size"));
1057 }
1058
1059 #[test]
1060 fn range_read_honors_limit() {
1061 let dir = tempfile::tempdir().unwrap();
1062 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2), unit(3)]).unwrap();
1063 write_segment_atomic(dir.path(), &segment).unwrap();
1064
1065 let units = read_units_since(dir.path(), identity(), 0, 2).unwrap();
1066 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
1067 assert_eq!(lsns, vec![1, 2]);
1068 }
1069
1070 #[test]
1071 fn range_read_stops_at_through_lsn() {
1072 let dir = tempfile::tempdir().unwrap();
1073 let segment = RetainedSegment::new(
1074 identity(),
1075 vec![unit(1), unit(2), unit(3), unit(4), unit(5)],
1076 )
1077 .unwrap();
1078 write_segment_atomic(dir.path(), &segment).unwrap();
1079
1080 let units = read_units_through(dir.path(), identity(), 1, 3, 10).unwrap();
1081 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
1082 assert_eq!(lsns, vec![2, 3]);
1083 }
1084
1085 #[test]
1086 fn retained_tail_progress_reports_contiguous_prefix_without_requiring_target() {
1087 let dir = tempfile::tempdir().unwrap();
1088 write_segment_atomic(
1089 dir.path(),
1090 &RetainedSegment::new(identity(), vec![unit(1), unit(2), unit(3)]).unwrap(),
1091 )
1092 .unwrap();
1093
1094 let progress = retained_tail_progress(dir.path(), identity(), 0, 5).unwrap();
1095 assert_eq!(progress.since_lsn, 0);
1096 assert_eq!(progress.requested_through_lsn, 5);
1097 assert_eq!(progress.contiguous_through_lsn, 3);
1098 assert_eq!(progress.units_available, 3);
1099 assert_eq!(progress.first_lsn, Some(1));
1100 assert_eq!(progress.last_lsn, Some(3));
1101 }
1102
1103 #[test]
1104 fn retained_tail_progress_rejects_gapped_retained_history() {
1105 let dir = tempfile::tempdir().unwrap();
1106 write_segment_atomic(
1107 dir.path(),
1108 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1109 )
1110 .unwrap();
1111 write_segment_atomic(
1112 dir.path(),
1113 &RetainedSegment::new(identity(), vec![unit(4), unit(5)]).unwrap(),
1114 )
1115 .unwrap();
1116
1117 let err = retained_tail_progress(dir.path(), identity(), 0, 5).unwrap_err();
1118 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1119 assert!(err.to_string().contains("gap"));
1120 }
1121
1122 #[test]
1123 fn concurrent_publish_same_range_is_no_clobber() {
1124 let dir = tempfile::tempdir().unwrap();
1125 let segment = Arc::new(RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap());
1126 let barrier = Arc::new(Barrier::new(8));
1127
1128 let mut handles = Vec::new();
1129 for _ in 0..8 {
1130 let dir = dir.path().to_path_buf();
1131 let segment = Arc::clone(&segment);
1132 let barrier = Arc::clone(&barrier);
1133 handles.push(std::thread::spawn(move || {
1134 barrier.wait();
1135 write_segment_atomic(&dir, &segment)
1136 }));
1137 }
1138
1139 let mut successes = 0usize;
1140 let mut already_exists = 0usize;
1141 for handle in handles {
1142 match handle.join().unwrap() {
1143 Ok(_) => successes += 1,
1144 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => already_exists += 1,
1145 Err(err) => panic!("unexpected publish error: {err}"),
1146 }
1147 }
1148 assert_eq!(successes, 1);
1149 assert_eq!(already_exists, 7);
1150
1151 let files = list_segment_files(dir.path()).unwrap();
1152 assert_eq!(files.len(), 1);
1153 let temp_files = std::fs::read_dir(dir.path())
1154 .unwrap()
1155 .filter_map(Result::ok)
1156 .filter(|entry| entry.file_name().to_string_lossy().contains(".tmp."))
1157 .count();
1158 assert_eq!(temp_files, 0);
1159 }
1160
1161 #[test]
1162 fn read_units_since_rejects_missing_segment_gap() {
1163 let dir = tempfile::tempdir().unwrap();
1164 write_segment_atomic(
1165 dir.path(),
1166 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1167 )
1168 .unwrap();
1169 write_segment_atomic(
1170 dir.path(),
1171 &RetainedSegment::new(identity(), vec![unit(4), unit(5)]).unwrap(),
1172 )
1173 .unwrap();
1174
1175 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1176 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1177 assert!(err.to_string().contains("gap"));
1178 }
1179
1180 #[test]
1181 fn validate_retained_tail_available_accepts_contiguous_tail() {
1182 let dir = tempfile::tempdir().unwrap();
1183 write_segment_atomic(
1184 dir.path(),
1185 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1186 )
1187 .unwrap();
1188 write_segment_atomic(
1189 dir.path(),
1190 &RetainedSegment::new(identity(), vec![unit(3), unit(4), unit(5)]).unwrap(),
1191 )
1192 .unwrap();
1193
1194 let availability = validate_retained_tail_available(dir.path(), identity(), 2, 5).unwrap();
1195 assert_eq!(availability.since_lsn, 2);
1196 assert_eq!(availability.through_lsn, 5);
1197 assert_eq!(availability.units_available, 3);
1198 assert_eq!(availability.first_lsn, Some(3));
1199 assert_eq!(availability.last_lsn, Some(5));
1200 }
1201
1202 #[test]
1203 fn validate_retained_tail_available_allows_empty_target_range() {
1204 let dir = tempfile::tempdir().unwrap();
1205 let availability = validate_retained_tail_available(dir.path(), identity(), 7, 7).unwrap();
1206 assert_eq!(availability.units_available, 0);
1207 assert_eq!(availability.first_lsn, None);
1208 assert_eq!(availability.last_lsn, None);
1209 }
1210
1211 #[test]
1212 fn validate_retained_tail_available_rejects_missing_tail() {
1213 let dir = tempfile::tempdir().unwrap();
1214 write_segment_atomic(
1215 dir.path(),
1216 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1217 )
1218 .unwrap();
1219 write_segment_atomic(
1220 dir.path(),
1221 &RetainedSegment::new(identity(), vec![unit(4), unit(5)]).unwrap(),
1222 )
1223 .unwrap();
1224
1225 let err = validate_retained_tail_available(dir.path(), identity(), 2, 5).unwrap_err();
1226 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1227 assert!(err.to_string().contains("gap"));
1228 }
1229
1230 #[test]
1231 fn validate_retained_tail_available_rejects_overlapping_segments() {
1232 let dir = tempfile::tempdir().unwrap();
1233 write_segment_atomic(
1234 dir.path(),
1235 &RetainedSegment::new(identity(), vec![unit(1), unit(2), unit(3)]).unwrap(),
1236 )
1237 .unwrap();
1238 write_segment_atomic(
1239 dir.path(),
1240 &RetainedSegment::new(identity(), vec![unit(3), unit(4)]).unwrap(),
1241 )
1242 .unwrap();
1243
1244 let err = validate_retained_tail_available(dir.path(), identity(), 0, 4).unwrap_err();
1245 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1246 assert!(err.to_string().contains("overlap"));
1247 }
1248
1249 #[test]
1250 fn read_units_since_rejects_overlapping_segments() {
1251 let dir = tempfile::tempdir().unwrap();
1252 write_segment_atomic(
1253 dir.path(),
1254 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1255 )
1256 .unwrap();
1257 write_segment_atomic(
1258 dir.path(),
1259 &RetainedSegment::new(identity(), vec![unit(2), unit(3)]).unwrap(),
1260 )
1261 .unwrap();
1262
1263 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1264 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1265 assert!(err.to_string().contains("overlap"));
1266 }
1267
1268 #[test]
1269 fn read_units_since_rejects_filename_header_range_mismatch() {
1270 let dir = tempfile::tempdir().unwrap();
1271 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
1272 std::fs::write(
1273 dir.path().join(segment_file_name(10, 11)),
1274 segment.to_bytes().unwrap(),
1275 )
1276 .unwrap();
1277
1278 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1279 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1280 assert!(err.to_string().contains("filename range"));
1281 }
1282
1283 #[test]
1284 fn read_units_since_rejects_mixed_database_identity() {
1285 let dir = tempfile::tempdir().unwrap();
1286 let wrong_identity = SegmentIdentity::current(*b"fedcba9876543210", 7);
1287 write_segment_atomic(
1288 dir.path(),
1289 &RetainedSegment::new(wrong_identity, vec![unit(1), unit(2)]).unwrap(),
1290 )
1291 .unwrap();
1292
1293 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1294 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1295 assert!(err.to_string().contains("identity"));
1296 }
1297
1298 #[test]
1299 fn read_units_since_rejects_mixed_primary_generation() {
1300 let dir = tempfile::tempdir().unwrap();
1301 let wrong_identity = SegmentIdentity::current(*b"0123456789abcdef", 8);
1302 write_segment_atomic(
1303 dir.path(),
1304 &RetainedSegment::new(wrong_identity, vec![unit(1), unit(2)]).unwrap(),
1305 )
1306 .unwrap();
1307
1308 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1309 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1310 assert!(err.to_string().contains("identity"));
1311 }
1312
1313 #[test]
1314 fn rejects_zero_database_id_and_primary_generation() {
1315 assert!(RetainedSegment::new(SegmentIdentity::current([0; 16], 1), vec![unit(1)]).is_err());
1316 assert!(RetainedSegment::new(
1317 SegmentIdentity::current(*b"0123456789abcdef", 0),
1318 vec![unit(1)]
1319 )
1320 .is_err());
1321 }
1322
1323 #[test]
1324 fn rejects_impossible_record_count_before_allocating() {
1325 let segment = RetainedSegment::new(identity(), vec![unit(1)]).unwrap();
1326 let mut bytes = segment.to_bytes().unwrap();
1327 bytes[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
1328 rewrite_footer_crc(&mut bytes);
1329
1330 let err = RetainedSegment::from_bytes(&bytes).unwrap_err();
1331 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1332 assert!(err.to_string().contains("record count"));
1333 }
1334
1335 fn identity_v(catalog_version: u16) -> SegmentIdentity {
1336 SegmentIdentity::with_catalog_version(*b"0123456789abcdef", 7, catalog_version)
1337 }
1338
1339 #[test]
1340 fn validate_identity_accepts_older_and_rejects_newer_catalog_version() {
1341 assert!(identity_v(LEGACY_CATALOG_VERSION).validate().is_ok());
1343 assert!(identity_v(CATALOG_VERSION).validate().is_ok());
1344 assert!(identity_v(0).validate().is_err());
1346 let err = identity_v(CATALOG_VERSION + 1).validate().unwrap_err();
1348 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1349 let msg = err.to_string();
1350 assert!(msg.contains(&format!("v{}", CATALOG_VERSION + 1)), "{msg}");
1351 assert!(msg.contains(&format!("max v{CATALOG_VERSION}")), "{msg}");
1352 }
1353
1354 #[test]
1355 fn v5_stamped_segment_reads_on_v6_binary() {
1356 let dir = tempfile::tempdir().unwrap();
1357 write_segment_atomic(
1358 dir.path(),
1359 &RetainedSegment::new(identity_v(LEGACY_CATALOG_VERSION), vec![unit(1), unit(2)])
1360 .unwrap(),
1361 )
1362 .unwrap();
1363 let units = read_units_since(dir.path(), identity(), 0, 10).unwrap();
1366 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
1367 assert_eq!(lsns, vec![1, 2]);
1368 }
1369
1370 #[test]
1371 fn catalog_version_increase_across_chain_is_accepted() {
1372 let dir = tempfile::tempdir().unwrap();
1373 write_segment_atomic(
1374 dir.path(),
1375 &RetainedSegment::new(identity_v(LEGACY_CATALOG_VERSION), vec![unit(1), unit(2)])
1376 .unwrap(),
1377 )
1378 .unwrap();
1379 write_segment_atomic(
1380 dir.path(),
1381 &RetainedSegment::new(identity_v(CATALOG_VERSION), vec![unit(3), unit(4)]).unwrap(),
1382 )
1383 .unwrap();
1384 let units = read_units_since(dir.path(), identity(), 0, 10).unwrap();
1386 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
1387 assert_eq!(lsns, vec![1, 2, 3, 4]);
1388 }
1389
1390 #[test]
1391 fn catalog_version_decrease_across_chain_is_rejected() {
1392 let dir = tempfile::tempdir().unwrap();
1393 write_segment_atomic(
1394 dir.path(),
1395 &RetainedSegment::new(identity_v(CATALOG_VERSION), vec![unit(1), unit(2)]).unwrap(),
1396 )
1397 .unwrap();
1398 write_segment_atomic(
1399 dir.path(),
1400 &RetainedSegment::new(identity_v(LEGACY_CATALOG_VERSION), vec![unit(3), unit(4)])
1401 .unwrap(),
1402 )
1403 .unwrap();
1404 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1405 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1406 assert!(err.to_string().contains("decreased across chain"), "{err}");
1407 }
1408
1409 fn rewrite_footer_crc(bytes: &mut [u8]) {
1410 let footer_start = bytes.len() - FOOTER_LEN;
1411 let crc = crc32fast::hash(&bytes[..footer_start]);
1412 bytes[footer_start + 4..footer_start + 8].copy_from_slice(&crc.to_le_bytes());
1413 }
1414}