nodedb_wal/record/
padding.rs1use super::header::HEADER_SIZE;
24use super::types::RecordType;
25use super::wal_record::{WalRecord, WalRecordArgs};
26use crate::align::{AlignedBuf, round_up};
27use crate::error::{Result, WalError};
28
29pub const MIN_PADDING_RECORD_SIZE: usize = HEADER_SIZE;
32
33pub fn padding_span(len: usize, alignment: usize) -> Option<usize> {
40 let gap = round_up(len, alignment) - len;
41 if gap == 0 {
42 return None;
43 }
44 if gap < MIN_PADDING_RECORD_SIZE {
45 Some(gap + alignment)
46 } else {
47 Some(gap)
48 }
49}
50
51pub fn padding_record(total_size: usize) -> Result<WalRecord> {
56 let payload_len = total_size
57 .checked_sub(HEADER_SIZE)
58 .ok_or(WalError::AlignmentViolation {
59 context: "alignment padding is narrower than a WAL record header",
60 required: HEADER_SIZE,
61 actual: total_size,
62 })?;
63
64 WalRecord::new(WalRecordArgs {
65 record_type: RecordType::Noop as u32,
66 lsn: 0,
67 tenant_id: 0,
68 vshard_id: 0,
69 database_id: 0,
70 payload: vec![0u8; payload_len],
71 encryption_key: None,
72 preamble_bytes: None,
73 })
74}
75
76pub(crate) fn pad_buffer_to_alignment(
85 buffer: &mut AlignedBuf,
86 alignment: usize,
87 context: &'static str,
88) -> Result<()> {
89 let Some(span) = padding_span(buffer.len(), alignment) else {
90 return Ok(());
91 };
92 if buffer.remaining() < span {
93 return Err(WalError::AlignmentViolation {
94 context,
95 required: span,
96 actual: buffer.remaining(),
97 });
98 }
99 let padding = padding_record(span)?;
100 buffer.write(&padding.header.to_bytes());
101 buffer.write(&padding.payload);
102 Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::align::DEFAULT_ALIGNMENT;
109
110 #[test]
111 fn aligned_length_needs_no_padding() {
112 assert_eq!(padding_span(0, DEFAULT_ALIGNMENT), None);
113 assert_eq!(padding_span(DEFAULT_ALIGNMENT, DEFAULT_ALIGNMENT), None);
114 assert_eq!(padding_span(2 * DEFAULT_ALIGNMENT, DEFAULT_ALIGNMENT), None);
115 }
116
117 #[test]
118 fn span_closes_the_gap_to_the_next_boundary() {
119 let span = padding_span(100, DEFAULT_ALIGNMENT).unwrap();
120 assert_eq!(span, DEFAULT_ALIGNMENT - 100);
121 assert_eq!((100 + span) % DEFAULT_ALIGNMENT, 0);
122 }
123
124 #[test]
125 fn narrow_gap_is_extended_by_a_whole_block() {
126 let len = DEFAULT_ALIGNMENT - 1;
129 let span = padding_span(len, DEFAULT_ALIGNMENT).unwrap();
130 assert!(span >= MIN_PADDING_RECORD_SIZE);
131 assert_eq!((len + span) % DEFAULT_ALIGNMENT, 0);
132 }
133
134 #[test]
135 fn every_unaligned_length_yields_a_framable_aligned_span() {
136 for len in 0..(2 * DEFAULT_ALIGNMENT) {
137 match padding_span(len, DEFAULT_ALIGNMENT) {
138 None => assert_eq!(len % DEFAULT_ALIGNMENT, 0),
139 Some(span) => {
140 assert!(
141 span >= MIN_PADDING_RECORD_SIZE,
142 "span {span} cannot be framed"
143 );
144 assert_eq!((len + span) % DEFAULT_ALIGNMENT, 0);
145 assert!(padding_record(span).is_ok());
146 }
147 }
148 }
149 }
150
151 #[test]
152 fn padding_record_occupies_exactly_its_span() {
153 let record = padding_record(512).unwrap();
154 assert_eq!(HEADER_SIZE + record.payload.len(), 512);
155 assert_eq!(record.header.lsn, 0);
156 assert_eq!(record.header.record_type, RecordType::Noop as u32);
157 assert!(record.verify_checksum().is_ok());
158 }
159
160 #[test]
161 fn span_below_a_header_is_rejected() {
162 assert!(matches!(
163 padding_record(HEADER_SIZE - 1),
164 Err(WalError::AlignmentViolation { .. })
165 ));
166 }
167}