hyperfile 0.9.1

A file-like interface abstraction layer provides random read and write capabilities for S3
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
use serde::{Deserialize, Serialize};
use crate::{BlockPtr, SegmentId, SegmentOffset};
use std::io::{Error, ErrorKind, Result};

const BLOCK_PTR_DUMMY: u64 = 0x3FFF_FFFF_FFFF_FFFF;
// a block ptr to a zero block
const BLOCK_PTR_ZERO_BLOCK: u64 = 0x3FFF_FFFF_0000_0000;

// mask
const BLOCK_PTR_LOCATION_MASK: u64 = 0xC000_0000_0000_0000;
const BLOCK_PTR_SEGMENT_ID_MASK: u64 = 0x3FFF_FFFF_0000_0000;
const BLOCK_PTR_SEGMENT_FLAT_OFFSET_MASK: u64 = 0x0000_0000_FFFF_FFFF;
const BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_MASK: u64 = 0x0000_0000_FFFF_C000; // in 4KiB
const BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_MASK: u64 = 0x0000_0000_0000_3FFF;
// `PartedSegment` reuses the micro-group field as the part index. Same bits,
// different meaning, which is safe only because the format byte tells the two
// apart: a container is one or the other for its whole life.
const BLOCK_PTR_PARTED_PART_MASK: u64 = 0x0000_0000_0000_3FFF;

const BLOCK_PTR_SEGMENT_ID_BIT_SHIFT: u32 = 32;
const BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_SHIFT: u32 = 14;
const BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_SHIFT: u32 = 4; // 16 entry per group
const BLOCK_PTR_SEGMENT_MG_OFFSET_ID_SHIFT: u32 = 12; // 4KiB

pub const BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_BITS: u32 = BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_SHIFT;
pub const BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_BITS: u32 = 32 - BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_BITS;

/// The highest checkpoint number a block pointer can name.
///
/// Every format keeps the checkpoint in the same 30-bit field, so this is the
/// limit for all of them, and a stored counter past it cannot have come from
/// this crate.
pub const BLOCK_PTR_MAX_CHECKPOINT: u64 = BLOCK_PTR_SEGMENT_ID_MASK >> BLOCK_PTR_SEGMENT_ID_BIT_SHIFT;

/// How many parts one checkpoint may be streamed as, under
/// [`BlockPtrFormat::PartedSegment`].
pub const BLOCK_PTR_PARTED_MAX_PARTS: usize = 1 << BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_BITS;

/// The largest one part may be. The in-part block index is the same 18 bits the
/// unparted format uses for the whole segment, so a part is bounded by what a
/// whole segment used to be — and a checkpoint is bounded by that times the
/// number of parts.
pub const BLOCK_PTR_PARTED_MAX_PART_BYTES: usize =
    (1 << BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_BITS) << BLOCK_PTR_SEGMENT_MG_OFFSET_ID_SHIFT;

// data on staging
const BLOCK_PTR_STAGING: u64 = 0x4000_0000_0000_0000;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[repr(u8)]
pub enum BlockPtrFormat {
    Nop = 0,
    Flat = 1,
    MicroGroup = 2,
    /// Like [`Self::MicroGroup`], but a checkpoint may be written as several
    /// objects — `<segid>.<part>` — instead of one.
    ///
    /// The part index goes where the micro-group index used to, which costs
    /// nothing because that field never took part in addressing: `decode` ignored
    /// it, and its only reader was a debug string. What a container gains is the
    /// ability to stream a checkpoint out in pieces and still name every block
    /// from the pointer alone.
    ///
    /// Part 0 is the summary, the metadata blocks and the inode, and it is
    /// written last — nothing else in the checkpoint can be described until the
    /// rest of it exists. Data blocks are in parts 1 and up. So part 0 being
    /// present is what says the stream finished, and a reader that finds parts
    /// without it is looking at a checkpoint that was abandoned.
    PartedSegment = 3,
}

impl BlockPtrFormat {
    /// Decode a format byte, or report one this build does not know.
    ///
    /// Separate from [`Self::from_u8`] because this byte can arrive from a
    /// persisted container written by another version, and a library has no
    /// business panicking over what it read from storage.
    #[inline]
    pub fn try_from_u8(data: u8) -> Result<Self> {
        match data {
            0 => Ok(Self::Nop),
            1 => Ok(Self::Flat),
            2 => Ok(Self::MicroGroup),
            3 => Ok(Self::PartedSegment),
            n => Err(Error::new(ErrorKind::InvalidData,
                format!("unknown block ptr format {}", n))),
        }
    }

    /// Decode a format byte, panicking on one this build does not know.
    ///
    /// For a byte this crate produced itself. Anything decoded from a
    /// container should use [`Self::try_from_u8`].
    #[inline]
    pub fn from_u8(data: u8) -> Self {
        Self::try_from_u8(data).expect("block ptr format")
    }

    #[inline]
    pub fn new_zero_block() -> BlockPtr {
        BLOCK_PTR_ZERO_BLOCK
    }

    #[inline]
    pub fn is_zero_block(blk_ptr: &BlockPtr) -> bool {
        *blk_ptr == BLOCK_PTR_ZERO_BLOCK
    }

    #[inline]
    pub fn dummy_value() -> BlockPtr {
        BLOCK_PTR_DUMMY
    }

    #[inline]
    pub fn is_dummy_value(blk_ptr: &BlockPtr) -> bool {
        *blk_ptr == BLOCK_PTR_DUMMY
    }

    #[inline]
    pub fn invalid_value() -> BlockPtr {
        BlockPtr::MIN
    }

    #[inline]
    pub fn is_invalid_value(blk_ptr: &BlockPtr) -> bool {
        *blk_ptr == BlockPtr::MIN
    }

    #[inline]
    pub fn is_on_staging(blk_ptr: &BlockPtr) -> bool {
        *blk_ptr & BLOCK_PTR_LOCATION_MASK == BLOCK_PTR_STAGING
    }

    /// Encode a location.
    ///
    /// `part` is which object of a streamed checkpoint the block landed in, and
    /// `offset` is its offset **within that object**. Every format but
    /// [`Self::PartedSegment`] writes one object per checkpoint and ignores
    /// `part`; for those, `offset` is the offset within the whole segment, which
    /// is the same thing.
    pub fn encode(segid: SegmentId, offset: SegmentOffset, seq: usize, fmt: &BlockPtrFormat) -> BlockPtr {
        let id = segid.seq_id() as u64;
        match fmt {
            Self::Nop => 0,
            Self::Flat => Self::encode_flat(id, offset, seq),
            Self::MicroGroup => Self::encode_micro_group(id, offset, seq),
            Self::PartedSegment => Self::encode_parted_segment(
                id, segid.part_id().unwrap_or(0), offset),
        }
    }

    /// Decode a location: which checkpoint, which of its objects, and where
    /// inside that object.
    ///
    /// The part is `None` for a format that writes one object per checkpoint,
    /// which is what says "the object has no part suffix" rather than "part 0".
    /// Those are different keys, and a container written before parting existed
    /// has the first kind.
    pub fn decode(blk_ptr: &BlockPtr, fmt: &BlockPtrFormat) -> (SegmentId, SegmentOffset) {
        match fmt {
            Self::Nop => (SegmentId::new(0), 0),
            Self::Flat => { let (s, o) = Self::decode_flat(blk_ptr); (SegmentId::new(s as u32), o) },
            Self::MicroGroup => { let (s, o) = Self::decode_micro_group(blk_ptr); (SegmentId::new(s as u32), o) },
            Self::PartedSegment => Self::decode_parted_segment(blk_ptr),
        }
    }

    /// Whether this format writes a checkpoint as several objects.
    #[inline]
    pub fn is_parted(&self) -> bool {
        matches!(self, Self::PartedSegment)
    }

    /// The most bytes one object may hold: past it, a pointer's offset field cannot
    /// name a block. Under [`Self::PartedSegment`] that bounds each part, and a
    /// checkpoint larger than it is written as several; every other format writes
    /// a checkpoint as one object, so for them it bounds the checkpoint.
    pub fn max_object_bytes(&self) -> usize {
        match self {
            Self::Nop => usize::MAX,
            Self::Flat => (BLOCK_PTR_SEGMENT_FLAT_OFFSET_MASK as usize) + 1,
            Self::MicroGroup | Self::PartedSegment => BLOCK_PTR_PARTED_MAX_PART_BYTES,
        }
    }

    #[inline]
    fn encode_parted_segment(segid: u64, part: u16, offset: SegmentOffset) -> BlockPtr {
        // Checked in every build: past either limit the value is masked into
        // another block's place, and what is stored reads back as the wrong bytes.
        // The flush lays checkpoints out so neither is reached.
        assert!((part as usize) < BLOCK_PTR_PARTED_MAX_PARTS,
            "part {} past the {} a pointer can name", part, BLOCK_PTR_PARTED_MAX_PARTS);
        assert!(offset < BLOCK_PTR_PARTED_MAX_PART_BYTES,
            "offset {} past the {} bytes one part can hold", offset, BLOCK_PTR_PARTED_MAX_PART_BYTES);
        BLOCK_PTR_STAGING |
        (segid << BLOCK_PTR_SEGMENT_ID_BIT_SHIFT) & BLOCK_PTR_SEGMENT_ID_MASK |
        (offset as u64 >> BLOCK_PTR_SEGMENT_MG_OFFSET_ID_SHIFT << BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_SHIFT) & BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_MASK |
        (part as u64 & BLOCK_PTR_PARTED_PART_MASK)
    }

    #[inline]
    fn decode_parted_segment(blk_ptr: &BlockPtr) -> (SegmentId, SegmentOffset) {
        // clear bit 63 and bit 62
        let p = blk_ptr & !BLOCK_PTR_LOCATION_MASK;
        // format:
        //   - bits 0-13 part index
        //   - bits 14-31 block index within that part
        //   - bits 32-61 for segid
        let part = (p & BLOCK_PTR_PARTED_PART_MASK) as u16;
        let seg_offset_id = (p & BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_MASK) >> BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_SHIFT;
        let segid = (p & BLOCK_PTR_SEGMENT_ID_MASK) >> BLOCK_PTR_SEGMENT_ID_BIT_SHIFT;

        // convert to bytes offset
        let offset = seg_offset_id << BLOCK_PTR_SEGMENT_MG_OFFSET_ID_SHIFT; // multiple 4KiB
        (SegmentId::with_part(segid as u32, part), offset as SegmentOffset)
    }

    #[inline]
    fn encode_flat(segid: u64, offset: SegmentOffset, _seq: usize) -> BlockPtr {
        assert!(offset < BlockPtrFormat::Flat.max_object_bytes(),
            "offset {} past what a flat pointer can name", offset);
        BLOCK_PTR_STAGING |
        (segid << BLOCK_PTR_SEGMENT_ID_BIT_SHIFT) & BLOCK_PTR_SEGMENT_ID_MASK |
        (offset as u64 & BLOCK_PTR_SEGMENT_FLAT_OFFSET_MASK)
    }

    #[inline]
    fn decode_flat(blk_ptr: &BlockPtr) -> (u64, SegmentOffset) {
        // clear bit 63 and bit 62
        let p = blk_ptr & !BLOCK_PTR_LOCATION_MASK;
        // format:
        //   - bits 0-31 offset
        //   - bits 32-61 for segid
        let offset = p & BLOCK_PTR_SEGMENT_FLAT_OFFSET_MASK;
        let segid = (p & BLOCK_PTR_SEGMENT_ID_MASK) >> BLOCK_PTR_SEGMENT_ID_BIT_SHIFT;
        (segid, offset as SegmentOffset)
    }

    #[inline]
    fn encode_micro_group(segid: u64, offset: SegmentOffset, seq: usize) -> BlockPtr {
        assert!(offset < BLOCK_PTR_PARTED_MAX_PART_BYTES,
            "offset {} past the {} bytes one segment can hold", offset, BLOCK_PTR_PARTED_MAX_PART_BYTES);
        BLOCK_PTR_STAGING |
        (segid << BLOCK_PTR_SEGMENT_ID_BIT_SHIFT) & BLOCK_PTR_SEGMENT_ID_MASK |
        (offset as u64 >> BLOCK_PTR_SEGMENT_MG_OFFSET_ID_SHIFT << BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_SHIFT) & BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_MASK |
        ((seq as u64 >> BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_SHIFT) & BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_MASK) // convert to group id
    }

    #[inline]
    fn decode_micro_group(blk_ptr: &BlockPtr) -> (u64, SegmentOffset) {
        // clear bit 63 and bit 62
        let p = blk_ptr & !BLOCK_PTR_LOCATION_MASK;
        // format:
        //   - bits 0-13 micro group id
        //   - bits 14-31 segment block index
        //   - bits 32-61 for segid
        let seg_offset_id = (p & BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_MASK) >> BLOCK_PTR_SEGMENT_MG_BLOCK_INDEX_SHIFT;
        let segid = (p & BLOCK_PTR_SEGMENT_ID_MASK) >> BLOCK_PTR_SEGMENT_ID_BIT_SHIFT;

        // convert to bytes offset
        let offset = seg_offset_id << BLOCK_PTR_SEGMENT_MG_OFFSET_ID_SHIFT; // multiple 4KiB
        (segid, offset as SegmentOffset)
    }

    #[inline]
    pub fn decode_micro_group_id(blk_ptr: &BlockPtr) -> u64 {
        blk_ptr & BLOCK_PTR_SEGMENT_MG_GROUP_INDEX_MASK
    }

    // segid location is fixed, not depend on specific format
    #[inline]
    pub fn decode_segid(blk_ptr: &BlockPtr) -> SegmentId {
        let p = blk_ptr & !BLOCK_PTR_LOCATION_MASK;
        let segid = (p & BLOCK_PTR_SEGMENT_ID_MASK) >> BLOCK_PTR_SEGMENT_ID_BIT_SHIFT;
        SegmentId::new(segid as u32)
    }
}

#[cfg(test)]
mod tests {

    #[test]
    fn each_format_names_its_own_object_limit() {
        assert_eq!(BlockPtrFormat::PartedSegment.max_object_bytes(), 1 << 30);
        assert_eq!(BlockPtrFormat::MicroGroup.max_object_bytes(), 1 << 30);
        assert_eq!(BlockPtrFormat::Flat.max_object_bytes(), 1 << 32);
        // the last offset of each is still named exactly
        for fmt in [BlockPtrFormat::PartedSegment, BlockPtrFormat::MicroGroup, BlockPtrFormat::Flat] {
            let off = fmt.max_object_bytes() - 4096;
            let p = BlockPtrFormat::encode(crate::SegmentId::new_from_cno(7).at_part(0), off, 0, &fmt);
            assert_eq!(BlockPtrFormat::decode(&p, &fmt).1, off, "{:?}", fmt);
        }
    }

    /// Past what a pointer can name, encoding refuses in every build instead of
    /// masking the offset into another block's place.
    #[test]
    #[should_panic(expected = "past the")]
    fn a_parted_offset_past_one_part_is_refused() {
        let _ = BlockPtrFormat::encode(crate::SegmentId::new_from_cno(1).at_part(0), 1 << 30, 0, &BlockPtrFormat::PartedSegment);
    }

    #[test]
    #[should_panic(expected = "past the")]
    fn a_micro_group_offset_past_one_segment_is_refused() {
        let _ = BlockPtrFormat::encode(crate::SegmentId::new_from_cno(1), 1 << 30, 0, &BlockPtrFormat::MicroGroup);
    }

    use super::*;

    // --- from_u8 ---

    #[test]
    fn from_u8_known_variants() {
        assert_eq!(BlockPtrFormat::from_u8(0), BlockPtrFormat::Nop);
        assert_eq!(BlockPtrFormat::from_u8(1), BlockPtrFormat::Flat);
        assert_eq!(BlockPtrFormat::from_u8(2), BlockPtrFormat::MicroGroup);
        assert_eq!(BlockPtrFormat::from_u8(3), BlockPtrFormat::PartedSegment);
    }

    #[test]
    #[should_panic(expected = "unknown block ptr format")]
    fn from_u8_invalid() {
        BlockPtrFormat::from_u8(4);
    }

    // --- sentinel values ---

    #[test]
    fn zero_block_sentinel() {
        let z = BlockPtrFormat::new_zero_block();
        assert!(BlockPtrFormat::is_zero_block(&z));
        assert!(!BlockPtrFormat::is_zero_block(&0));
        assert!(!BlockPtrFormat::is_dummy_value(&z));
    }

    #[test]
    fn dummy_value_sentinel() {
        let d = BlockPtrFormat::dummy_value();
        assert!(BlockPtrFormat::is_dummy_value(&d));
        assert!(!BlockPtrFormat::is_dummy_value(&0));
        assert!(!BlockPtrFormat::is_zero_block(&d));
    }

    #[test]
    fn invalid_value_sentinel() {
        let iv = BlockPtrFormat::invalid_value();
        assert!(BlockPtrFormat::is_invalid_value(&iv));
        assert_eq!(iv, BlockPtr::MIN);
    }

    // --- Flat encode/decode round-trip ---

    #[test]
    fn flat_round_trip_basic() {
        let segid = SegmentId::new(1);
        let offset: SegmentOffset = 4096;
        let ptr = BlockPtrFormat::encode(segid, offset, 0, &BlockPtrFormat::Flat);
        assert!(BlockPtrFormat::is_on_staging(&ptr));
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::Flat), (segid, offset));
    }

    #[test]
    fn flat_round_trip_large_segid() {
        let segid = SegmentId::new(0x3FFF_FFFF); // max 30-bit segid
        let offset: SegmentOffset = 0xFFFF_FFFF; // max 32-bit offset
        let ptr = BlockPtrFormat::encode(segid, offset, 0, &BlockPtrFormat::Flat);
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::Flat), (segid, offset));
    }

    #[test]
    fn flat_round_trip_zero() {
        let ptr = BlockPtrFormat::encode(SegmentId::new(0), 0, 0, &BlockPtrFormat::Flat);
        assert!(BlockPtrFormat::is_on_staging(&ptr));
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::Flat), (SegmentId::new(0), 0));
    }

    // --- PartedSegment encode/decode round-trip ---

    #[test]
    fn parted_round_trip_carries_the_part() {
        for part in [0u16, 1, 2, 255, 4095, (BLOCK_PTR_PARTED_MAX_PARTS - 1) as u16] {
            let segid = SegmentId::with_part(12345, part);
            let offset: SegmentOffset = 8192;
            let ptr = BlockPtrFormat::encode(segid, offset, 0, &BlockPtrFormat::PartedSegment);
            assert!(BlockPtrFormat::is_on_staging(&ptr));
            let (got, o) = BlockPtrFormat::decode(&ptr, &BlockPtrFormat::PartedSegment);
            assert_eq!(got, segid, "part {} did not survive", part);
            assert_eq!(got.part_id(), Some(part));
            assert_eq!(o, offset);
        }
    }

    #[test]
    fn parted_round_trip_at_the_field_edges() {
        // Max 30-bit checkpoint, last part, and the largest 4 KiB-aligned offset
        // the in-part block index can name.
        let segid = SegmentId::with_part(0x3FFF_FFFF, (BLOCK_PTR_PARTED_MAX_PARTS - 1) as u16);
        let offset: SegmentOffset = BLOCK_PTR_PARTED_MAX_PART_BYTES - 4096;
        let ptr = BlockPtrFormat::encode(segid, offset, 0, &BlockPtrFormat::PartedSegment);
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::PartedSegment), (segid, offset));
    }

    /// The part occupies the field the unparted format spends on a micro-group
    /// index, so the same bits mean different things and only the format byte
    /// tells them apart. A pointer read under the wrong format must not look
    /// plausible by accident.
    #[test]
    fn parted_and_unparted_do_not_share_a_reading() {
        let ptr = BlockPtrFormat::encode(
            SegmentId::with_part(7, 3), 8192, 0, &BlockPtrFormat::PartedSegment);
        let (parted, _) = BlockPtrFormat::decode(&ptr, &BlockPtrFormat::PartedSegment);
        assert_eq!(parted.part_id(), Some(3));
        // Read as unparted, the part bits are a group index and report no part
        // at all — which is why the two formats are never mixed in a container.
        let (plain, _) = BlockPtrFormat::decode(&ptr, &BlockPtrFormat::MicroGroup);
        assert_eq!(plain.part_id(), None);
    }

    #[test]
    fn only_the_parted_format_is_parted() {
        assert!(BlockPtrFormat::PartedSegment.is_parted());
        assert!(!BlockPtrFormat::MicroGroup.is_parted());
        assert!(!BlockPtrFormat::Flat.is_parted());
        assert!(!BlockPtrFormat::Nop.is_parted());
    }

    /// Part 0 is the summary's, and no data block is ever addressed there — the
    /// flush starts data at part 1. Nothing enforces that in the encoding, so
    /// this pins the constants the flush relies on.
    #[test]
    fn summary_part_is_zero_and_data_starts_after_it() {
        use crate::segment::Segment;
        assert_eq!(Segment::SUMMARY_PART, 0);
        assert_eq!(Segment::FIRST_DATA_PART, 1);
        assert!(Segment::FIRST_DATA_PART > Segment::SUMMARY_PART);
    }

    // --- MicroGroup encode/decode round-trip ---

    #[test]
    fn micro_group_round_trip_basic() {
        // offset must be 4KiB aligned for MicroGroup
        let segid = SegmentId::new(5);
        let offset: SegmentOffset = 8192; // 2 * 4KiB
        let seq = 32; // seq >= 16 to produce group_id >= 1
        let ptr = BlockPtrFormat::encode(segid, offset, seq, &BlockPtrFormat::MicroGroup);
        assert!(BlockPtrFormat::is_on_staging(&ptr));
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::MicroGroup), (segid, offset));
    }

    #[test]
    fn micro_group_round_trip_zero_offset() {
        let ptr = BlockPtrFormat::encode(SegmentId::new(1), 0, 0, &BlockPtrFormat::MicroGroup);
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::MicroGroup), (SegmentId::new(1), 0));
    }

    #[test]
    fn micro_group_group_id() {
        let seq = 48; // group_id = 48 >> 4 = 3
        let ptr = BlockPtrFormat::encode(SegmentId::new(1), 4096, seq, &BlockPtrFormat::MicroGroup);
        assert_eq!(BlockPtrFormat::decode_micro_group_id(&ptr), 3);
    }

    // --- decode_segid (format-independent) ---

    #[test]
    fn decode_segid_consistent_across_formats() {
        let segid = SegmentId::new(42);
        let flat_ptr = BlockPtrFormat::encode(segid, 4096, 0, &BlockPtrFormat::Flat);
        let mg_ptr = BlockPtrFormat::encode(segid, 4096, 0, &BlockPtrFormat::MicroGroup);
        assert_eq!(BlockPtrFormat::decode_segid(&flat_ptr), segid);
        assert_eq!(BlockPtrFormat::decode_segid(&mg_ptr), segid);
    }

    // --- Nop ---

    #[test]
    fn nop_encode_decode() {
        let ptr = BlockPtrFormat::encode(SegmentId::new(99), 1234, 0, &BlockPtrFormat::Nop);
        assert_eq!(ptr, 0);
        assert_eq!(BlockPtrFormat::decode(&ptr, &BlockPtrFormat::Nop), (SegmentId::new(0), 0));
    }
}