Skip to content

Commit 478d5bf

Browse files
committed
refactor(segment): collapse footer format v1/v2 into a single version
Every segment has carried an encrypted extent index since the format gained one, so the v1/no-index and v2/indexed footer layouts no longer describe a real distinction. Fold both into one FORMAT_VERSION with a fixed field layout, drop the version-conditional branches from the footer encoder/decoder and metadata validator, and let index_page_count == 0 stand for "no extents" without a separate version tag.
1 parent 932aa81 commit 478d5bf

14 files changed

Lines changed: 166 additions & 211 deletions

File tree

src/pager/format/page_kind.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ pub enum PageKind {
2222
/// before an `apply_incremental` header swap to record the segment
2323
/// promotions and tombstones that must be completed after the swap.
2424
ApplyJournal = 0x08,
25-
/// v2 overflow root page. Carries a `refcount: u32` in its body header
26-
/// before the `next` pointer. Chain pages (not the root) continue to use
27-
/// `PageKind::Overflow`. This crate always writes the count as 1 —
25+
/// First page of an overflow chain. Carries a `refcount: u32` in its body
26+
/// header before the `next` pointer, which is what distinguishes it from
27+
/// the chain's later pages — those use `PageKind::Overflow`. This crate
28+
/// always writes the count as 1 —
2829
/// snapshot lifetime comes from commit-tagged page reclamation — but the
2930
/// release path still honours a larger count so a root produced elsewhere
3031
/// decodes correctly.

src/pager/format/segment_footer/decode.rs

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use crate::{RealmId, Result};
44

55
use super::auth::{constant_time_eq, footer_aad, mac_hk};
66
use super::fields::{
7-
FOOTER_CLEARTEXT_END_V1, FOOTER_CLEARTEXT_END_V2, FOOTER_FIELDS_END_V1, FOOTER_FIELDS_END_V2,
8-
MAGIC, MANIFEST_TAG_LEN, SegmentFooterFields, max_manifest_len, max_manifest_len_v2,
7+
FOOTER_CLEARTEXT_END, FOOTER_FIELDS_END, FORMAT_VERSION, MAGIC, MANIFEST_TAG_LEN,
8+
SegmentFooterFields, max_manifest_len,
99
};
1010

1111
pub fn decode_segment_footer(
@@ -17,47 +17,27 @@ pub fn decode_segment_footer(
1717
let parsed = parse_cleartext(bytes, page_size)?;
1818
authenticate_cleartext(bytes, hk, &parsed)?;
1919
let (fields, ciphertext_end, tag_end) = assemble_fields(bytes, &parsed, cipher)?;
20-
let manifest = authenticate_manifest(
21-
bytes,
22-
cipher,
23-
&fields,
24-
parsed.cleartext_end,
25-
ciphertext_end,
26-
tag_end,
27-
)?;
20+
let manifest = authenticate_manifest(bytes, cipher, &fields, ciphertext_end, tag_end)?;
2821
Ok((fields, manifest))
2922
}
3023

3124
struct ParsedFooter {
32-
fields_end: usize,
33-
cleartext_end: usize,
34-
max_manifest: usize,
3525
manifest_offset: u32,
3626
manifest_len: usize,
3727
fields: SegmentFooterFields,
3828
}
3929

4030
fn parse_cleartext(bytes: &[u8], page_size: usize) -> Result<ParsedFooter> {
41-
if bytes.len() != page_size || page_size < FOOTER_CLEARTEXT_END_V1 + MANIFEST_TAG_LEN {
31+
if bytes.len() != page_size || page_size < FOOTER_CLEARTEXT_END + MANIFEST_TAG_LEN {
4232
return Err(PagedbError::Unsupported);
4333
}
4434
if bytes[..8] != MAGIC {
4535
return Err(PagedbError::footer_framing_invalid("magic"));
4636
}
4737
let format_version = u16_le(&bytes[8..10]);
48-
let (fields_end, cleartext_end, max_manifest) = match format_version {
49-
1 => (
50-
FOOTER_FIELDS_END_V1,
51-
FOOTER_CLEARTEXT_END_V1,
52-
max_manifest_len(page_size),
53-
),
54-
2 if page_size >= FOOTER_CLEARTEXT_END_V2 + MANIFEST_TAG_LEN => (
55-
FOOTER_FIELDS_END_V2,
56-
FOOTER_CLEARTEXT_END_V2,
57-
max_manifest_len_v2(page_size),
58-
),
59-
_ => return Err(PagedbError::Unsupported),
60-
};
38+
if format_version != FORMAT_VERSION {
39+
return Err(PagedbError::footer_framing_invalid("format_version"));
40+
}
6141
let mut offset = 10;
6242
let cipher_id = bytes[offset];
6343
offset += 1;
@@ -80,26 +60,10 @@ fn parse_cleartext(bytes: &[u8], page_size: usize) -> Result<ParsedFooter> {
8060
let manifest_len = usize::try_from(u32_le(&bytes[offset..offset + 4]))
8161
.map_err(|_| PagedbError::Unsupported)?;
8262
offset += 4;
83-
let (index_start_page, index_page_count) = if format_version == 2 {
84-
(
85-
u64_le(&bytes[offset..offset + 8]),
86-
u32_le(&bytes[offset + 8..offset + 12]),
87-
)
88-
} else {
89-
(0, 0)
90-
};
91-
debug_assert_eq!(
92-
if format_version == 2 {
93-
offset + 12
94-
} else {
95-
offset
96-
},
97-
fields_end
98-
);
63+
let index_start_page = u64_le(&bytes[offset..offset + 8]);
64+
let index_page_count = u32_le(&bytes[offset + 8..offset + 12]);
65+
debug_assert_eq!(offset + 12, FOOTER_FIELDS_END);
9966
Ok(ParsedFooter {
100-
fields_end,
101-
cleartext_end,
102-
max_manifest,
10367
manifest_offset,
10468
manifest_len,
10569
fields: SegmentFooterFields {
@@ -123,13 +87,13 @@ fn authenticate_cleartext(
12387
hk: &crate::crypto::keys::DerivedKey,
12488
parsed: &ParsedFooter,
12589
) -> Result<()> {
126-
let mac = mac_hk(hk, &bytes[..parsed.fields_end])?;
127-
let valid_mac = constant_time_eq(&mac, &bytes[parsed.fields_end..parsed.cleartext_end]);
90+
let mac = mac_hk(hk, &bytes[..FOOTER_FIELDS_END])?;
91+
let valid_mac = constant_time_eq(&mac, &bytes[FOOTER_FIELDS_END..FOOTER_CLEARTEXT_END]);
12892
let expected_offset =
129-
u32::try_from(parsed.cleartext_end).map_err(|_| PagedbError::Unsupported)?;
93+
u32::try_from(FOOTER_CLEARTEXT_END).map_err(|_| PagedbError::Unsupported)?;
13094
if !valid_mac
13195
|| parsed.manifest_offset != expected_offset
132-
|| parsed.manifest_len > parsed.max_manifest
96+
|| parsed.manifest_len > max_manifest_len(bytes.len())
13397
{
13498
return Err(PagedbError::footer_framing_invalid(
13599
"manifest_offset_or_length",
@@ -152,8 +116,7 @@ fn assemble_fields(
152116
},
153117
));
154118
}
155-
let ciphertext_end = parsed
156-
.cleartext_end
119+
let ciphertext_end = FOOTER_CLEARTEXT_END
157120
.checked_add(parsed.manifest_len)
158121
.ok_or_else(|| PagedbError::arithmetic_overflow("footer manifest end"))?;
159122
let tag_end = ciphertext_end
@@ -169,7 +132,6 @@ fn authenticate_manifest(
169132
bytes: &[u8],
170133
cipher: &Cipher,
171134
fields: &SegmentFooterFields,
172-
cleartext_end: usize,
173135
ciphertext_end: usize,
174136
tag_end: usize,
175137
) -> Result<Vec<u8>> {
@@ -181,7 +143,7 @@ fn authenticate_manifest(
181143
let mut file_id = [0u8; 6];
182144
file_id.copy_from_slice(&fields.segment_id[..6]);
183145
let nonce = Nonce::from_parts(file_id, nonce_counter);
184-
let mut manifest = bytes[cleartext_end..ciphertext_end].to_vec();
146+
let mut manifest = bytes[FOOTER_CLEARTEXT_END..ciphertext_end].to_vec();
185147
let mut tag = [0u8; MANIFEST_TAG_LEN];
186148
tag.copy_from_slice(&bytes[ciphertext_end..tag_end]);
187149
cipher

src/pager/format/segment_footer/encode.rs

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ use crate::errors::PagedbError;
44

55
use super::auth::{footer_aad, mac_hk};
66
use super::fields::{
7-
FOOTER_CLEARTEXT_END_V1, FOOTER_CLEARTEXT_END_V2, FOOTER_FIELDS_END_V1, FOOTER_FIELDS_END_V2,
8-
FOOTER_HEADER_MAC_LEN, MANIFEST_TAG_LEN, SegmentFooterFields, max_manifest_len,
9-
max_manifest_len_v2,
7+
FOOTER_CLEARTEXT_END, FOOTER_FIELDS_END, FOOTER_HEADER_MAC_LEN, FORMAT_VERSION,
8+
MANIFEST_TAG_LEN, SegmentFooterFields, max_manifest_len,
109
};
1110

1211
pub fn encode_segment_footer(
@@ -16,27 +15,21 @@ pub fn encode_segment_footer(
1615
cipher: &Cipher,
1716
page_size: usize,
1817
) -> Result<Vec<u8>> {
19-
let (fields_end, cleartext_end) = match fields.format_version {
20-
1 => (FOOTER_FIELDS_END_V1, FOOTER_CLEARTEXT_END_V1),
21-
2 => (FOOTER_FIELDS_END_V2, FOOTER_CLEARTEXT_END_V2),
22-
_ => return Err(PagedbError::Unsupported),
23-
};
24-
if page_size < cleartext_end + MANIFEST_TAG_LEN {
18+
if fields.format_version != FORMAT_VERSION {
2519
return Err(PagedbError::Unsupported);
2620
}
27-
let max_manifest = if fields.format_version == 1 {
28-
max_manifest_len(page_size)
29-
} else {
30-
max_manifest_len_v2(page_size)
31-
};
32-
if manifest.len() > max_manifest {
21+
if page_size < FOOTER_CLEARTEXT_END + MANIFEST_TAG_LEN {
22+
return Err(PagedbError::Unsupported);
23+
}
24+
if manifest.len() > max_manifest_len(page_size) {
3325
return Err(PagedbError::ManifestTooLarge);
3426
}
3527
if cipher.id().as_byte() != fields.cipher_id {
3628
return Err(PagedbError::Unsupported);
3729
}
3830

39-
let manifest_offset = u32::try_from(cleartext_end).map_err(|_| PagedbError::Unsupported)?;
31+
let manifest_offset =
32+
u32::try_from(FOOTER_CLEARTEXT_END).map_err(|_| PagedbError::Unsupported)?;
4033
let manifest_len = u32::try_from(manifest.len()).map_err(|_| PagedbError::ManifestTooLarge)?;
4134
let mut out = vec![0u8; page_size];
4235
let mut offset = 0usize;
@@ -64,17 +57,15 @@ pub fn encode_segment_footer(
6457
offset += 4;
6558
out[offset..offset + 4].copy_from_slice(&manifest_len.to_le_bytes());
6659
offset += 4;
67-
if fields.format_version == 2 {
68-
out[offset..offset + 8].copy_from_slice(&fields.index_start_page.to_le_bytes());
69-
offset += 8;
70-
out[offset..offset + 4].copy_from_slice(&fields.index_page_count.to_le_bytes());
71-
offset += 4;
72-
}
73-
debug_assert_eq!(offset, fields_end);
60+
out[offset..offset + 8].copy_from_slice(&fields.index_start_page.to_le_bytes());
61+
offset += 8;
62+
out[offset..offset + 4].copy_from_slice(&fields.index_page_count.to_le_bytes());
63+
offset += 4;
64+
debug_assert_eq!(offset, FOOTER_FIELDS_END);
7465
let mac = mac_hk(hk, &out[..offset])?;
7566
out[offset..offset + FOOTER_HEADER_MAC_LEN].copy_from_slice(&mac);
7667
offset += FOOTER_HEADER_MAC_LEN;
77-
debug_assert_eq!(offset, cleartext_end);
68+
debug_assert_eq!(offset, FOOTER_CLEARTEXT_END);
7869

7970
let nonce_counter = fields
8071
.final_counter
@@ -84,14 +75,14 @@ pub fn encode_segment_footer(
8475
let mut file_id = [0u8; 6];
8576
file_id.copy_from_slice(&fields.segment_id[..6]);
8677
let nonce = Nonce::from_parts(file_id, nonce_counter);
87-
let manifest_end = cleartext_end
78+
let manifest_end = FOOTER_CLEARTEXT_END
8879
.checked_add(manifest.len())
8980
.ok_or_else(|| PagedbError::arithmetic_overflow("footer manifest end"))?;
90-
out[cleartext_end..manifest_end].copy_from_slice(manifest);
81+
out[FOOTER_CLEARTEXT_END..manifest_end].copy_from_slice(manifest);
9182
let tag = cipher.encrypt(
9283
&nonce,
9384
&footer_aad(fields),
94-
&mut out[cleartext_end..manifest_end],
85+
&mut out[FOOTER_CLEARTEXT_END..manifest_end],
9586
)?;
9687
let tag_end = manifest_end
9788
.checked_add(MANIFEST_TAG_LEN)
Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
use crate::RealmId;
22

33
pub const MAGIC: [u8; 8] = *b"PAGESEAL";
4-
pub const FOOTER_FIELDS_END_V1: usize = 99;
5-
pub const FOOTER_FIELDS_END_V2: usize = FOOTER_FIELDS_END_V1 + 12;
4+
5+
/// The only `format_version` this decoder accepts. The field is persisted so a
6+
/// later layout can be introduced without ambiguity; any other value is a
7+
/// footer this build cannot interpret and is rejected as corruption.
8+
pub const FORMAT_VERSION: u16 = 1;
9+
10+
/// End of the MAC-covered cleartext field block.
11+
pub const FOOTER_FIELDS_END: usize = 111;
612
pub const FOOTER_HEADER_MAC_LEN: usize = 16;
7-
pub const FOOTER_CLEARTEXT_END_V1: usize = FOOTER_FIELDS_END_V1 + FOOTER_HEADER_MAC_LEN;
8-
pub const FOOTER_CLEARTEXT_END_V2: usize = FOOTER_FIELDS_END_V2 + FOOTER_HEADER_MAC_LEN;
9-
/// Unversioned aliases for the v1 boundaries. Encode and decode always select
10-
/// a boundary from the footer's own `format_version`, so only the tamper tests
11-
/// — which forge a v1 footer and flip the byte at each boundary — name these.
12-
#[cfg(test)]
13-
pub const FOOTER_FIELDS_END: usize = FOOTER_FIELDS_END_V1;
14-
#[cfg(test)]
15-
pub const FOOTER_CLEARTEXT_END: usize = FOOTER_CLEARTEXT_END_V1;
13+
/// End of the cleartext prefix — fields plus their MAC. The encrypted manifest
14+
/// starts here.
15+
pub const FOOTER_CLEARTEXT_END: usize = FOOTER_FIELDS_END + FOOTER_HEADER_MAC_LEN;
1616
pub const MANIFEST_TAG_LEN: usize = 16;
1717

1818
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -32,10 +32,5 @@ pub struct SegmentFooterFields {
3232

3333
#[must_use]
3434
pub const fn max_manifest_len(page_size: usize) -> usize {
35-
page_size - FOOTER_CLEARTEXT_END_V1 - MANIFEST_TAG_LEN
36-
}
37-
38-
#[must_use]
39-
pub const fn max_manifest_len_v2(page_size: usize) -> usize {
40-
page_size - FOOTER_CLEARTEXT_END_V2 - MANIFEST_TAG_LEN
35+
page_size - FOOTER_CLEARTEXT_END - MANIFEST_TAG_LEN
4136
}

src/pager/format/segment_footer/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ mod fields;
77

88
pub use decode::decode_segment_footer;
99
pub use encode::encode_segment_footer;
10-
pub use fields::{SegmentFooterFields, max_manifest_len, max_manifest_len_v2};
10+
pub use fields::{FORMAT_VERSION, SegmentFooterFields, max_manifest_len};
1111
// Layout boundaries the footer tests assert against directly.
1212
#[cfg(test)]
1313
pub use fields::{FOOTER_CLEARTEXT_END, FOOTER_FIELDS_END};

src/pager/format/segment_footer/tests.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ use crate::RealmId;
22
use crate::crypto::Cipher;
33
use crate::crypto::kdf::{derive_dek, derive_hk, derive_mk};
44
use crate::crypto::nonce::Nonce;
5-
use crate::errors::PagedbError;
5+
use crate::errors::{CorruptionDetail, PagedbError};
66

77
use super::{
8-
FOOTER_CLEARTEXT_END, FOOTER_FIELDS_END, SegmentFooterFields, decode_segment_footer,
9-
encode_segment_footer, max_manifest_len,
8+
FOOTER_CLEARTEXT_END, FOOTER_FIELDS_END, FORMAT_VERSION, SegmentFooterFields,
9+
decode_segment_footer, encode_segment_footer, max_manifest_len,
1010
};
1111

1212
const PAGE_SIZE: usize = 4096;
@@ -20,7 +20,7 @@ fn keys() -> (crate::crypto::keys::DerivedKey, Cipher) {
2020

2121
fn fields() -> SegmentFooterFields {
2222
SegmentFooterFields {
23-
format_version: 1,
23+
format_version: FORMAT_VERSION,
2424
cipher_id: 1,
2525
segment_id: [9; 16],
2626
parent_file_id: [1; 16],
@@ -73,6 +73,39 @@ fn rejects_footer_nonce_beyond_u48_counter_space() {
7373
assert!(encode_segment_footer(&exhausted, b"manifest", &header, &cipher, PAGE_SIZE).is_ok());
7474
}
7575

76+
#[test]
77+
fn rejects_footer_whose_declared_format_version_is_not_the_accepted_one() {
78+
let (header, cipher) = keys();
79+
let encoded =
80+
encode_segment_footer(&fields(), b"manifest", &header, &cipher, PAGE_SIZE).unwrap();
81+
for declared in [FORMAT_VERSION - 1, FORMAT_VERSION + 1, u16::MAX] {
82+
let mut forged = encoded.clone();
83+
forged[8..10].copy_from_slice(&declared.to_le_bytes());
84+
assert!(
85+
matches!(
86+
decode_segment_footer(&forged, &header, &cipher, PAGE_SIZE),
87+
Err(PagedbError::Corruption(
88+
CorruptionDetail::FooterFramingInvalid {
89+
field: "format_version"
90+
}
91+
))
92+
),
93+
"format_version={declared} must be rejected as unreadable framing"
94+
);
95+
}
96+
}
97+
98+
#[test]
99+
fn refuses_to_encode_a_footer_under_an_unaccepted_format_version() {
100+
let (header, cipher) = keys();
101+
let mut unknown = fields();
102+
unknown.format_version = FORMAT_VERSION + 1;
103+
assert!(matches!(
104+
encode_segment_footer(&unknown, b"manifest", &header, &cipher, PAGE_SIZE),
105+
Err(PagedbError::Unsupported)
106+
));
107+
}
108+
76109
#[test]
77110
fn rejects_header_mac_and_manifest_tampering() {
78111
let (header, cipher) = keys();

0 commit comments

Comments
 (0)