1use crate::{checksum_skipping, get_u32, get_u64, put_u32, put_u64};
14use yo_common::{Code, Error, Result};
15
16pub const PAGE_HEADER_LEN: usize = 32;
18
19pub const PAGE_MAGIC: u32 = 0x594F_4C47;
21
22const CRC_OFFSET: usize = 28;
24
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27pub struct PageHeader {
28 pub shard: u32,
32 pub page_addr: u64,
36 pub used: u32,
38 pub dead_bytes: u32,
42 pub epoch: u32,
44}
45
46impl PageHeader {
47 pub fn encode(&self, page: &mut [u8]) {
53 assert!(page.len() >= PAGE_HEADER_LEN, "a page holds its own header");
54 put_u32(page, 0, PAGE_MAGIC);
55 put_u32(page, 4, self.shard);
56 put_u64(page, 8, self.page_addr);
57 put_u32(page, 16, self.used);
58 put_u32(page, 20, self.dead_bytes);
59 put_u32(page, 24, self.epoch);
60 let crc = checksum_skipping(&page[..CRC_OFFSET + 4], CRC_OFFSET);
61 put_u32(page, CRC_OFFSET, crc);
62 }
63
64 pub fn decode(page: &[u8]) -> Result<PageHeader> {
66 if page.len() < PAGE_HEADER_LEN {
67 return Err(Error::new(Code::Invalid, "shorter than a page header"));
68 }
69 let magic = get_u32(page, 0);
70 if magic != PAGE_MAGIC {
71 return Err(Error::new(Code::Corrupt, "this segment is not a log page")
72 .with_detail(format!("magic={magic:#010x} want={PAGE_MAGIC:#010x}")));
73 }
74 let want = get_u32(page, CRC_OFFSET);
75 let got = checksum_skipping(&page[..CRC_OFFSET + 4], CRC_OFFSET);
76 if want != got {
77 return Err(
78 Error::new(Code::Corrupt, "log page header checksum mismatch")
79 .with_detail(format!("stored={want:#010x} computed={got:#010x}")),
80 );
81 }
82 let h = PageHeader {
83 shard: get_u32(page, 4),
84 page_addr: get_u64(page, 8),
85 used: get_u32(page, 16),
86 dead_bytes: get_u32(page, 20),
87 epoch: get_u32(page, 24),
88 };
89 if h.dead_bytes > h.used {
90 return Err(
91 Error::new(Code::Corrupt, "more dead bytes than written bytes")
92 .with_detail(format!("dead_bytes={} used={}", h.dead_bytes, h.used)),
93 );
94 }
95 Ok(h)
96 }
97
98 #[must_use]
103 pub fn dead_fraction(&self) -> f64 {
104 if self.used == 0 {
105 return 0.0;
106 }
107 f64::from(self.dead_bytes) / f64::from(self.used)
108 }
109
110 #[must_use]
112 pub fn wants_compaction(&self) -> bool {
113 self.dead_fraction() > 0.5
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::DEFAULT_PAGE_SIZE;
121
122 fn a_page() -> Vec<u8> {
123 let mut page = vec![0u8; DEFAULT_PAGE_SIZE as usize];
124 PageHeader {
125 shard: 3,
126 page_addr: 32 * 1024 * 1024,
127 used: 4096,
128 dead_bytes: 1024,
129 epoch: 9,
130 }
131 .encode(&mut page);
132 page
133 }
134
135 #[test]
136 fn the_magic_reads_as_yolg_in_a_hex_dump() {
137 let page = a_page();
138 assert_eq!(&page[..4], b"GLOY", "little endian, so reversed on disk");
139 assert_eq!(PAGE_MAGIC.to_be_bytes(), *b"YOLG");
140 }
141
142 #[test]
143 fn a_page_header_round_trips() {
144 let page = a_page();
145 let h = PageHeader::decode(&page).unwrap();
146 assert_eq!(h.shard, 3);
147 assert_eq!(h.page_addr, 32 * 1024 * 1024);
148 assert_eq!(h.used, 4096);
149 assert_eq!(h.dead_bytes, 1024);
150 assert_eq!(h.epoch, 9);
151 }
152
153 #[test]
154 fn every_field_lands_where_the_specification_says() {
155 let page = a_page();
156 assert_eq!(get_u32(&page, 0), PAGE_MAGIC);
157 assert_eq!(get_u32(&page, 4), 3);
158 assert_eq!(get_u64(&page, 8), 32 * 1024 * 1024);
159 assert_eq!(get_u32(&page, 16), 4096);
160 assert_eq!(get_u32(&page, 20), 1024);
161 assert_eq!(get_u32(&page, 24), 9);
162 }
163
164 #[test]
165 fn the_checksum_covers_the_header_and_stops_there() {
166 let mut page = a_page();
167 page[PAGE_HEADER_LEN] = 0xff;
170 page[9000] = 0xff;
171 assert!(PageHeader::decode(&page).is_ok());
172
173 for i in 0..PAGE_HEADER_LEN {
174 let mut bad = page.clone();
175 bad[i] ^= 0x10;
176 assert!(PageHeader::decode(&bad).is_err(), "byte {i} was not caught");
177 }
178 }
179
180 #[test]
181 fn a_segment_that_is_not_a_log_page_says_so() {
182 let mut page = a_page();
183 put_u32(&mut page, 0, 0x1234_5678);
184 let err = PageHeader::decode(&page).unwrap_err();
185 assert_eq!(err.code(), Code::Corrupt);
186 assert!(err.detail().unwrap().contains("0x12345678"));
187 }
188
189 #[test]
190 fn more_dead_than_written_is_refused() {
191 let mut page = vec![0u8; 64];
192 PageHeader {
193 used: 100,
194 dead_bytes: 200,
195 ..PageHeader::default()
196 }
197 .encode(&mut page);
198 let err = PageHeader::decode(&page).unwrap_err();
199 assert_eq!(err.code(), Code::Corrupt);
200 }
201
202 #[test]
203 fn the_compaction_trigger_is_a_dead_fraction_over_a_half() {
204 let mut h = PageHeader {
205 used: 1000,
206 ..PageHeader::default()
207 };
208 h.dead_bytes = 500;
209 assert!(!h.wants_compaction(), "exactly a half does not trigger");
210 h.dead_bytes = 501;
211 assert!(h.wants_compaction());
212
213 let empty = PageHeader::default();
214 assert_eq!(empty.dead_fraction(), 0.0);
215 assert!(!empty.wants_compaction());
216 }
217
218 #[test]
219 fn a_short_buffer_is_an_error_and_not_a_panic() {
220 assert_eq!(
221 PageHeader::decode(&[0u8; 8]).unwrap_err().code(),
222 Code::Invalid
223 );
224 }
225}