idb/innodb/record.rs
1//! Row-level record parsing for InnoDB compact and redundant formats.
2//!
3//! InnoDB stores rows in either compact (MySQL 5.0+) or redundant (pre-5.0)
4//! record format. Each record has a header containing info bits, record type,
5//! heap number, and next-record pointer.
6//!
7//! This module provides [`RecordType`] classification, [`walk_compact_records`]
8//! and [`walk_redundant_records`] to traverse the singly-linked record chain
9//! within an INDEX page, starting from the infimum record.
10
11use byteorder::{BigEndian, ByteOrder};
12
13use crate::innodb::constants::*;
14
15/// Record type extracted from the info bits.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum RecordType {
18 /// Ordinary user record (leaf page).
19 Ordinary,
20 /// Node pointer record (non-leaf page).
21 NodePtr,
22 /// Infimum system record.
23 Infimum,
24 /// Supremum system record.
25 Supremum,
26}
27
28impl RecordType {
29 /// Convert a 3-bit status value from the record header to a `RecordType`.
30 ///
31 /// Only the lowest 3 bits of `val` are used.
32 ///
33 /// # Examples
34 ///
35 /// ```
36 /// use idb::innodb::record::RecordType;
37 ///
38 /// assert_eq!(RecordType::from_u8(0), RecordType::Ordinary);
39 /// assert_eq!(RecordType::from_u8(1), RecordType::NodePtr);
40 /// assert_eq!(RecordType::from_u8(2), RecordType::Infimum);
41 /// assert_eq!(RecordType::from_u8(3), RecordType::Supremum);
42 ///
43 /// // Only the lowest 3 bits are used, so 0x08 maps to Ordinary
44 /// assert_eq!(RecordType::from_u8(0x08), RecordType::Ordinary);
45 ///
46 /// assert_eq!(RecordType::from_u8(0).name(), "REC_STATUS_ORDINARY");
47 /// ```
48 pub fn from_u8(val: u8) -> Self {
49 match val & 0x07 {
50 0 => RecordType::Ordinary,
51 1 => RecordType::NodePtr,
52 2 => RecordType::Infimum,
53 3 => RecordType::Supremum,
54 _ => RecordType::Ordinary,
55 }
56 }
57
58 /// Returns the MySQL source-style name for this record type (e.g. `"REC_STATUS_ORDINARY"`).
59 pub fn name(&self) -> &'static str {
60 match self {
61 RecordType::Ordinary => "REC_STATUS_ORDINARY",
62 RecordType::NodePtr => "REC_STATUS_NODE_PTR",
63 RecordType::Infimum => "REC_STATUS_INFIMUM",
64 RecordType::Supremum => "REC_STATUS_SUPREMUM",
65 }
66 }
67}
68
69/// Parsed compact (new-style) record header.
70///
71/// In compact format, 5 bytes precede each record:
72/// - Byte 0: info bits (delete mark, min_rec flag) + n_owned upper nibble
73/// - Bytes 1-2: heap_no (13 bits) + rec_type (3 bits)
74/// - Bytes 3-4: next record offset (signed, relative)
75#[derive(Debug, Clone)]
76pub struct CompactRecordHeader {
77 /// Number of records owned by this record in the page directory.
78 pub n_owned: u8,
79 /// Delete mark flag.
80 pub delete_mark: bool,
81 /// Min-rec flag (leftmost record on a non-leaf level).
82 pub min_rec: bool,
83 /// Record's position in the heap.
84 pub heap_no: u16,
85 /// Record type.
86 pub rec_type: RecordType,
87 /// Relative offset to the next record (signed).
88 pub next_offset: i16,
89}
90
91impl CompactRecordHeader {
92 /// Parse a compact record header from the 5 bytes preceding the record origin.
93 ///
94 /// `data` should point to the start of the 5-byte extra header.
95 ///
96 /// # Examples
97 ///
98 /// ```
99 /// use idb::innodb::record::{CompactRecordHeader, RecordType};
100 /// use byteorder::{BigEndian, ByteOrder};
101 ///
102 /// let mut data = vec![0u8; 5];
103 /// // byte 0: info_bits(4) | n_owned(4)
104 /// // delete_mark=1 (bit 5), n_owned=2 (bits 0-3) => 0x22
105 /// data[0] = 0x22;
106 /// // bytes 1-2: heap_no=7 (7<<3=56), rec_type=0 (Ordinary) => 56
107 /// BigEndian::write_u16(&mut data[1..3], 7 << 3);
108 /// // bytes 3-4: next_offset = 42
109 /// BigEndian::write_i16(&mut data[3..5], 42);
110 ///
111 /// let hdr = CompactRecordHeader::parse(&data).unwrap();
112 /// assert_eq!(hdr.n_owned, 2);
113 /// assert!(hdr.delete_mark);
114 /// assert!(!hdr.min_rec);
115 /// assert_eq!(hdr.heap_no, 7);
116 /// assert_eq!(hdr.rec_type, RecordType::Ordinary);
117 /// assert_eq!(hdr.next_offset, 42);
118 /// ```
119 pub fn parse(data: &[u8]) -> Option<Self> {
120 if data.len() < REC_N_NEW_EXTRA_BYTES {
121 return None;
122 }
123
124 // Byte 0 layout: [info_bits(4) | n_owned(4)]
125 // Info bits (upper nibble): bit 5 = delete_mark, bit 4 = min_rec
126 // n_owned (lower nibble): bits 0-3
127 let byte0 = data[0];
128 let n_owned = byte0 & 0x0F;
129 let delete_mark = (byte0 & 0x20) != 0;
130 let min_rec = (byte0 & 0x10) != 0;
131
132 let two_bytes = BigEndian::read_u16(&data[1..3]);
133 let rec_type = RecordType::from_u8((two_bytes & 0x07) as u8);
134 let heap_no = (two_bytes >> 3) & 0x1FFF;
135
136 let next_offset = BigEndian::read_i16(&data[3..5]);
137
138 Some(CompactRecordHeader {
139 n_owned,
140 delete_mark,
141 min_rec,
142 heap_no,
143 rec_type,
144 next_offset,
145 })
146 }
147}
148
149/// Parsed redundant (old-style) record header.
150///
151/// In redundant format, 6 bytes precede each record:
152/// - Byte 0: info bits (delete mark, min_rec flag) + n_owned
153/// - Bytes 1-2: heap_no (13 bits) + rec_type (3 bits)
154/// - Bytes 2-3: n_fields (10 bits) + one_byte_offs flag (1 bit) (overlaps byte 2)
155/// - Bytes 4-5: next record offset (unsigned, absolute within page)
156#[derive(Debug, Clone)]
157pub struct RedundantRecordHeader {
158 /// Number of records owned by this record in the page directory.
159 pub n_owned: u8,
160 /// Delete mark flag.
161 pub delete_mark: bool,
162 /// Min-rec flag (leftmost record on a non-leaf level).
163 pub min_rec: bool,
164 /// Record's position in the heap.
165 pub heap_no: u16,
166 /// Record type.
167 pub rec_type: RecordType,
168 /// Absolute offset to the next record within the page (unsigned).
169 pub next_offset: u16,
170 /// Number of fields in this record.
171 pub n_fields: u16,
172 /// Whether field end offsets use 1 byte (true) or 2 bytes (false).
173 pub one_byte_offs: bool,
174}
175
176impl RedundantRecordHeader {
177 /// Parse a redundant record header from the 6 bytes preceding the record origin.
178 ///
179 /// `data` should point to the start of the 6-byte extra header.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use idb::innodb::record::{RedundantRecordHeader, RecordType};
185 /// use byteorder::{BigEndian, ByteOrder};
186 ///
187 /// let mut data = vec![0u8; 6];
188 /// // byte 0: info_bits(4) | n_owned(4) — n_owned=1, no flags
189 /// data[0] = 0x01;
190 /// // bytes 1-2: heap_no=5, rec_type=Ordinary(0) => (5 << 3) | 0 = 40
191 /// BigEndian::write_u16(&mut data[1..3], 5 << 3);
192 /// // bytes 2-3: n_fields=3, one_byte_offs=true => (3 << 6) | 0x20 = 0x00E0
193 /// // But byte 2 is shared — we set bytes 2-3 after bytes 1-2
194 /// // For this test, set byte 3 separately to encode n_fields in lower byte
195 /// data[3] = (3 << 6) as u8; // n_fields low bits + one_byte_offs=0
196 /// // bytes 4-5: next_offset = 200 (absolute)
197 /// BigEndian::write_u16(&mut data[4..6], 200);
198 ///
199 /// let hdr = RedundantRecordHeader::parse(&data).unwrap();
200 /// assert_eq!(hdr.n_owned, 1);
201 /// assert!(!hdr.delete_mark);
202 /// assert_eq!(hdr.heap_no, 5);
203 /// assert_eq!(hdr.rec_type, RecordType::Ordinary);
204 /// assert_eq!(hdr.next_offset, 200);
205 /// ```
206 pub fn parse(data: &[u8]) -> Option<Self> {
207 if data.len() < REC_N_OLD_EXTRA_BYTES {
208 return None;
209 }
210
211 // Byte 0: info_bits(4) | n_owned(4) — same layout as compact
212 let byte0 = data[0];
213 let n_owned = byte0 & 0x0F;
214 let delete_mark = (byte0 & 0x20) != 0;
215 let min_rec = (byte0 & 0x10) != 0;
216
217 // Bytes 1-2: heap_no (13 bits) + rec_type (3 bits) — same as compact
218 let heap_status = BigEndian::read_u16(&data[1..3]);
219 let rec_type = RecordType::from_u8((heap_status & 0x07) as u8);
220 let heap_no = (heap_status >> 3) & 0x1FFF;
221
222 // Bytes 2-3: n_fields (10 bits) + one_byte_offs_flag (1 bit) + unused (5 bits)
223 // Byte 2 is shared with the heap_no/rec_type word above.
224 let nf_word = BigEndian::read_u16(&data[2..4]);
225 let n_fields = (nf_word >> 6) & 0x03FF;
226 let one_byte_offs = (nf_word & 0x20) != 0;
227
228 // Bytes 4-5: next record offset (absolute, unsigned)
229 let next_offset = BigEndian::read_u16(&data[4..6]);
230
231 Some(RedundantRecordHeader {
232 n_owned,
233 delete_mark,
234 min_rec,
235 heap_no,
236 rec_type,
237 next_offset,
238 n_fields,
239 one_byte_offs,
240 })
241 }
242}
243
244/// Unified record header for both compact and redundant formats.
245#[derive(Debug, Clone)]
246pub enum RecordHeader {
247 /// Compact (new-style, MySQL 5.0+) record header.
248 Compact(CompactRecordHeader),
249 /// Redundant (old-style, pre-MySQL 5.0) record header.
250 Redundant(RedundantRecordHeader),
251}
252
253impl RecordHeader {
254 /// Number of records owned by this record in the page directory.
255 pub fn n_owned(&self) -> u8 {
256 match self {
257 Self::Compact(h) => h.n_owned,
258 Self::Redundant(h) => h.n_owned,
259 }
260 }
261
262 /// Delete mark flag.
263 pub fn delete_mark(&self) -> bool {
264 match self {
265 Self::Compact(h) => h.delete_mark,
266 Self::Redundant(h) => h.delete_mark,
267 }
268 }
269
270 /// Min-rec flag.
271 pub fn min_rec(&self) -> bool {
272 match self {
273 Self::Compact(h) => h.min_rec,
274 Self::Redundant(h) => h.min_rec,
275 }
276 }
277
278 /// Heap number.
279 pub fn heap_no(&self) -> u16 {
280 match self {
281 Self::Compact(h) => h.heap_no,
282 Self::Redundant(h) => h.heap_no,
283 }
284 }
285
286 /// Record type.
287 pub fn rec_type(&self) -> RecordType {
288 match self {
289 Self::Compact(h) => h.rec_type,
290 Self::Redundant(h) => h.rec_type,
291 }
292 }
293
294 /// Raw next-offset value as stored in the header (i16 for display).
295 /// For compact format this is relative; for redundant it is absolute.
296 pub fn next_offset_raw(&self) -> i16 {
297 match self {
298 Self::Compact(h) => h.next_offset,
299 Self::Redundant(h) => h.next_offset as i16,
300 }
301 }
302}
303
304/// A record position on a page, with its parsed header.
305#[derive(Debug, Clone)]
306pub struct RecordInfo {
307 /// Absolute offset of the record origin within the page.
308 pub offset: usize,
309 /// Parsed record header.
310 pub header: RecordHeader,
311}
312
313/// Walk all user records on a compact-format INDEX page.
314///
315/// Starts from infimum and follows next-record offsets until reaching supremum.
316/// Returns a list of record positions (excluding infimum/supremum).
317///
318/// # Examples
319///
320/// ```no_run
321/// use idb::innodb::record::walk_compact_records;
322/// use idb::innodb::tablespace::Tablespace;
323///
324/// let mut ts = Tablespace::open("table.ibd").unwrap();
325/// let page = ts.read_page(3).unwrap();
326/// let records = walk_compact_records(&page);
327/// for rec in &records {
328/// println!("Record at offset {}, type: {}", rec.offset, rec.header.rec_type().name());
329/// }
330/// ```
331pub fn walk_compact_records(page_data: &[u8]) -> Vec<RecordInfo> {
332 let mut records = Vec::new();
333
334 // Infimum record origin is at PAGE_NEW_INFIMUM (99)
335 let infimum_origin = PAGE_NEW_INFIMUM;
336 if page_data.len() < infimum_origin + 2 {
337 return records;
338 }
339
340 // Read infimum's next-record offset (at infimum_origin - 2, relative to origin)
341 let infimum_extra_start = infimum_origin - REC_N_NEW_EXTRA_BYTES;
342 if page_data.len() < infimum_extra_start + REC_N_NEW_EXTRA_BYTES {
343 return records;
344 }
345
346 let infimum_hdr = match CompactRecordHeader::parse(&page_data[infimum_extra_start..]) {
347 Some(h) => h,
348 None => return records,
349 };
350
351 // Follow the linked list
352 let mut current_offset = infimum_origin;
353 let mut next_rel = infimum_hdr.next_offset;
354
355 // Safety: limit iterations to prevent infinite loops
356 let max_iter = page_data.len();
357 let mut iterations = 0;
358
359 loop {
360 if iterations > max_iter {
361 break;
362 }
363 iterations += 1;
364
365 // Calculate next record's absolute offset
366 let next_abs = (current_offset as i32 + next_rel as i32) as usize;
367 if next_abs < REC_N_NEW_EXTRA_BYTES || next_abs >= page_data.len() {
368 break;
369 }
370
371 // Parse the record header (5 bytes before the origin)
372 let extra_start = next_abs - REC_N_NEW_EXTRA_BYTES;
373 if extra_start + REC_N_NEW_EXTRA_BYTES > page_data.len() {
374 break;
375 }
376
377 let hdr = match CompactRecordHeader::parse(&page_data[extra_start..]) {
378 Some(h) => h,
379 None => break,
380 };
381
382 // If we've reached supremum, stop
383 if hdr.rec_type == RecordType::Supremum {
384 break;
385 }
386
387 next_rel = hdr.next_offset;
388 records.push(RecordInfo {
389 offset: next_abs,
390 header: RecordHeader::Compact(hdr),
391 });
392 current_offset = next_abs;
393
394 // next_offset of 0 means end of list
395 if next_rel == 0 {
396 break;
397 }
398 }
399
400 records
401}
402
403/// Walk all user records on a redundant-format INDEX page.
404///
405/// Starts from the old-style infimum and follows absolute next-record offsets
406/// until reaching supremum. Returns a list of record positions (excluding
407/// infimum/supremum).
408pub fn walk_redundant_records(page_data: &[u8]) -> Vec<RecordInfo> {
409 let mut records = Vec::new();
410
411 let infimum_origin = PAGE_OLD_INFIMUM;
412 if page_data.len() < infimum_origin + 2 {
413 return records;
414 }
415
416 let infimum_extra_start = infimum_origin - REC_N_OLD_EXTRA_BYTES;
417 if page_data.len() < infimum_extra_start + REC_N_OLD_EXTRA_BYTES {
418 return records;
419 }
420
421 let infimum_hdr = match RedundantRecordHeader::parse(&page_data[infimum_extra_start..]) {
422 Some(h) => h,
423 None => return records,
424 };
425
426 // In redundant format, next_offset is absolute within the page
427 let mut next_abs = infimum_hdr.next_offset as usize;
428
429 // Safety: limit iterations to prevent infinite loops
430 let max_iter = page_data.len();
431 let mut iterations = 0;
432
433 loop {
434 if iterations > max_iter {
435 break;
436 }
437 iterations += 1;
438
439 if next_abs < REC_N_OLD_EXTRA_BYTES || next_abs >= page_data.len() {
440 break;
441 }
442
443 let extra_start = next_abs - REC_N_OLD_EXTRA_BYTES;
444 if extra_start + REC_N_OLD_EXTRA_BYTES > page_data.len() {
445 break;
446 }
447
448 let hdr = match RedundantRecordHeader::parse(&page_data[extra_start..]) {
449 Some(h) => h,
450 None => break,
451 };
452
453 // Supremum or offset 0 means end
454 if hdr.rec_type == RecordType::Supremum {
455 break;
456 }
457
458 let current_next = hdr.next_offset as usize;
459 records.push(RecordInfo {
460 offset: next_abs,
461 header: RecordHeader::Redundant(hdr),
462 });
463
464 if current_next == 0 {
465 break;
466 }
467 next_abs = current_next;
468 }
469
470 records
471}
472
473/// Parse the variable-length field lengths from a compact record's null bitmap
474/// and variable-length header. Returns the field data starting offset.
475///
476/// For SDI records and other known-format records, callers can use the
477/// record offset directly since field positions are fixed.
478pub fn read_variable_field_lengths(
479 page_data: &[u8],
480 record_origin: usize,
481 n_nullable: usize,
482 n_variable: usize,
483) -> Option<(Vec<bool>, Vec<usize>)> {
484 // The variable-length header grows backwards from the record origin,
485 // before the 5-byte compact extra header.
486 // Layout (backwards from origin - 5):
487 // - null bitmap: ceil(n_nullable / 8) bytes
488 // - variable-length field lengths: 1 or 2 bytes each
489
490 let null_bitmap_bytes = n_nullable.div_ceil(8);
491 let mut pos = record_origin - REC_N_NEW_EXTRA_BYTES;
492
493 // Read null bitmap
494 if pos < null_bitmap_bytes {
495 return None;
496 }
497 pos -= null_bitmap_bytes;
498 let mut nulls = Vec::with_capacity(n_nullable);
499 for i in 0..n_nullable {
500 let byte_idx = pos + (i / 8);
501 let bit_idx = i % 8;
502 if byte_idx >= page_data.len() {
503 return None;
504 }
505 nulls.push((page_data[byte_idx] & (1 << bit_idx)) != 0);
506 }
507
508 // Read variable-length field lengths
509 let mut var_lengths = Vec::with_capacity(n_variable);
510 for _ in 0..n_variable {
511 if pos == 0 {
512 return None;
513 }
514 pos -= 1;
515 if pos >= page_data.len() {
516 return None;
517 }
518 let len_byte = page_data[pos] as usize;
519 if len_byte & 0x80 != 0 {
520 // 2-byte length
521 if pos == 0 {
522 return None;
523 }
524 pos -= 1;
525 if pos >= page_data.len() {
526 return None;
527 }
528 let high_byte = page_data[pos] as usize;
529 let total_len = ((len_byte & 0x3F) << 8) | high_byte;
530 var_lengths.push(total_len);
531 } else {
532 var_lengths.push(len_byte);
533 }
534 }
535
536 Some((nulls, var_lengths))
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use byteorder::ByteOrder;
543
544 #[test]
545 fn test_record_type_from_u8() {
546 assert_eq!(RecordType::from_u8(0), RecordType::Ordinary);
547 assert_eq!(RecordType::from_u8(1), RecordType::NodePtr);
548 assert_eq!(RecordType::from_u8(2), RecordType::Infimum);
549 assert_eq!(RecordType::from_u8(3), RecordType::Supremum);
550 }
551
552 #[test]
553 fn test_compact_record_header_parse() {
554 // Build a 5-byte compact header:
555 // byte0: [info_bits(4) | n_owned(4)]
556 // n_owned=1 in lower nibble, no info bits => 0x01
557 // bytes 1-2: heap_no=5 (5<<3=0x0028), rec_type=0 => 0x0028
558 // bytes 3-4: next_offset = 30 => 0x001E
559 let mut data = vec![0u8; 5];
560 data[0] = 0x01; // n_owned=1, no delete, no min_rec
561 BigEndian::write_u16(&mut data[1..3], 5 << 3); // heap_no=5, type=0
562 BigEndian::write_i16(&mut data[3..5], 30); // next=30
563
564 let hdr = CompactRecordHeader::parse(&data).unwrap();
565 assert_eq!(hdr.n_owned, 1);
566 assert!(!hdr.delete_mark);
567 assert!(!hdr.min_rec);
568 assert_eq!(hdr.heap_no, 5);
569 assert_eq!(hdr.rec_type, RecordType::Ordinary);
570 assert_eq!(hdr.next_offset, 30);
571 }
572
573 #[test]
574 fn test_compact_record_header_with_flags() {
575 let mut data = vec![0u8; 5];
576 // n_owned=3 (0x30), delete_mark (0x20), min_rec (0x10)
577 // => 0x30 | 0x20 | 0x10 = 0x70... wait, n_owned is bits 4-7 so n_owned=3 is 0x30
578 // delete_mark is bit 5 (0x20), min_rec is bit 4 (0x10)
579 // But if n_owned=3 takes bits 4-7, that's 0x30, which conflicts with bit 5 for delete.
580 // Actually in InnoDB: byte0 has info_bits in upper 4 bits and... let me recheck.
581 // The layout is: [info_bits(4) | n_owned(4)]
582 // info_bits: bit 7=unused, bit 6=unused, bit 5=delete_mark, bit 4=min_rec
583 // n_owned: bits 0-3
584 // So: delete_mark=1, min_rec=0, n_owned=2 => 0x20 | 0x02 = 0x22
585 data[0] = 0x22; // delete_mark=1, n_owned=2
586 BigEndian::write_u16(&mut data[1..3], (10 << 3) | 1); // heap_no=10, type=node_ptr
587 BigEndian::write_i16(&mut data[3..5], -50); // negative offset
588
589 let hdr = CompactRecordHeader::parse(&data).unwrap();
590 assert_eq!(hdr.n_owned, 2);
591 assert!(hdr.delete_mark);
592 assert!(!hdr.min_rec);
593 assert_eq!(hdr.heap_no, 10);
594 assert_eq!(hdr.rec_type, RecordType::NodePtr);
595 assert_eq!(hdr.next_offset, -50);
596 }
597
598 #[test]
599 fn test_redundant_record_header_parse() {
600 let mut data = vec![0u8; 6];
601 // byte 0: n_owned=2, delete_mark=1 => 0x22
602 data[0] = 0x22;
603 // bytes 1-2: heap_no=8, rec_type=Ordinary(0) => (8 << 3) = 64
604 BigEndian::write_u16(&mut data[1..3], 8 << 3);
605 // bytes 2-3 overlap — byte 2 is shared; set byte 3 for n_fields encoding
606 // n_fields=5, one_byte_offs=false: (5 << 6) = 320 = 0x0140
607 // But byte 2 is already set from the heap_no write, so we only set byte 3
608 data[3] = 0x40; // lower byte: n_fields bits 1-0 shifted + one_byte_offs=0
609 // bytes 4-5: next_offset = 300 (absolute)
610 BigEndian::write_u16(&mut data[4..6], 300);
611
612 let hdr = RedundantRecordHeader::parse(&data).unwrap();
613 assert_eq!(hdr.n_owned, 2);
614 assert!(hdr.delete_mark);
615 assert_eq!(hdr.heap_no, 8);
616 assert_eq!(hdr.rec_type, RecordType::Ordinary);
617 assert_eq!(hdr.next_offset, 300);
618 }
619
620 #[test]
621 fn test_redundant_record_header_no_flags() {
622 let mut data = vec![0u8; 6];
623 data[0] = 0x01; // n_owned=1, no flags
624 BigEndian::write_u16(&mut data[1..3], 3 << 3); // heap_no=3, Ordinary
625 BigEndian::write_u16(&mut data[4..6], 150);
626
627 let hdr = RedundantRecordHeader::parse(&data).unwrap();
628 assert_eq!(hdr.n_owned, 1);
629 assert!(!hdr.delete_mark);
630 assert!(!hdr.min_rec);
631 assert_eq!(hdr.heap_no, 3);
632 assert_eq!(hdr.rec_type, RecordType::Ordinary);
633 assert_eq!(hdr.next_offset, 150);
634 }
635
636 #[test]
637 fn test_record_header_enum_accessors() {
638 let mut data = vec![0u8; 5];
639 data[0] = 0x22; // delete_mark, n_owned=2
640 BigEndian::write_u16(&mut data[1..3], 5 << 3);
641 BigEndian::write_i16(&mut data[3..5], 42);
642
643 let compact = CompactRecordHeader::parse(&data).unwrap();
644 let header = RecordHeader::Compact(compact);
645 assert_eq!(header.n_owned(), 2);
646 assert!(header.delete_mark());
647 assert_eq!(header.heap_no(), 5);
648 assert_eq!(header.rec_type(), RecordType::Ordinary);
649 assert_eq!(header.next_offset_raw(), 42);
650 }
651}