Skip to main content

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}