Skip to content

Commit 4799acd

Browse files
committed
fix(universaldb): align versionstamp operand encoding
1 parent 63a3fd9 commit 4799acd

2 files changed

Lines changed: 92 additions & 17 deletions

File tree

engine/packages/universaldb/src/versionstamp.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,26 @@ pub fn substitute_versionstamp(
6262
versionstamp: Versionstamp,
6363
) -> Result<(), String> {
6464
const VERSIONSTAMP_MARKER: u8 = 0x33;
65-
const VERSIONSTAMP_SIZE: usize = 12;
65+
const VERSIONSTAMP_SIZE: usize = 10;
6666

6767
if packed_data.len() < 4 {
6868
return Err("Packed data too short to contain versionstamp offset".to_string());
6969
}
7070

71-
let offset_bytes = packed_data.split_off(packed_data.len() - 4);
71+
let data_len = packed_data.len() - 4;
72+
let offset_bytes = &packed_data[data_len..];
7273
let offset = u32::from_le_bytes([
7374
offset_bytes[0],
7475
offset_bytes[1],
7576
offset_bytes[2],
7677
offset_bytes[3],
7778
]) as usize;
7879

79-
if offset >= packed_data.len() {
80+
if offset >= data_len {
8081
return Err(format!(
8182
"Invalid versionstamp offset: {} exceeds data length {}",
8283
offset,
83-
packed_data.len()
84+
data_len
8485
));
8586
}
8687

@@ -99,18 +100,19 @@ pub fn substitute_versionstamp(
99100

100101
let versionstamp_end = versionstamp_start + VERSIONSTAMP_SIZE;
101102

102-
if versionstamp_end > packed_data.len() {
103+
if versionstamp_end > data_len {
103104
return Err("Versionstamp extends beyond data bounds".to_string());
104105
}
105106

106107
let existing_bytes = &packed_data[versionstamp_start..versionstamp_end];
107108
if existing_bytes[0..10] != [0xff; 10] {
108-
// Versionstamp is already complete, nothing to do
109+
packed_data.truncate(data_len);
109110
return Ok(());
110111
}
111112

112113
let versionstamp_bytes = versionstamp.as_bytes();
113-
packed_data[versionstamp_start..versionstamp_end].copy_from_slice(versionstamp_bytes);
114+
packed_data[versionstamp_start..versionstamp_end].copy_from_slice(&versionstamp_bytes[..10]);
115+
packed_data.truncate(data_len);
114116

115117
Ok(())
116118
}
@@ -123,7 +125,8 @@ pub fn substitute_raw_versionstamp(
123125
return Err("Packed data too short to contain versionstamp offset".to_string());
124126
}
125127

126-
let offset_bytes = data.split_off(data.len() - 4);
128+
let data_len = data.len() - 4;
129+
let offset_bytes = &data[data_len..];
127130
let offset = u32::from_le_bytes([
128131
offset_bytes[0],
129132
offset_bytes[1],
@@ -135,15 +138,16 @@ pub fn substitute_raw_versionstamp(
135138
.checked_add(versionstamp_len)
136139
.ok_or_else(|| "Versionstamp offset overflowed".to_string())?;
137140

138-
if versionstamp_end > data.len() {
141+
if versionstamp_end > data_len {
139142
return Err(format!(
140143
"Invalid versionstamp offset: {} exceeds data length {}",
141144
offset,
142-
data.len()
145+
data_len
143146
));
144147
}
145148

146149
data[offset..versionstamp_end].copy_from_slice(&versionstamp.as_bytes()[..versionstamp_len]);
150+
data.truncate(data_len);
147151

148152
Ok(data)
149153
}

engine/packages/universaldb/tests/versionstamp.rs

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fn test_generate_versionstamp() {
1818

1919
#[test]
2020
fn test_substitute_versionstamp_success() {
21-
let incomplete = Versionstamp::from([0xff; 12]);
21+
let incomplete = Versionstamp::incomplete(100);
2222
let tuple = vec![
2323
Element::String("mykey".into()),
2424
Element::Versionstamp(incomplete),
@@ -99,9 +99,83 @@ fn test_substitute_versionstamp_already_complete() {
9999
assert!(result.is_ok());
100100
}
101101

102+
#[test]
103+
fn test_substitute_raw_versionstamp_trims_explicit_operand_offset() {
104+
let versionstamp = generate_versionstamp(100);
105+
let mut param = b"prefix".to_vec();
106+
let offset = param.len() as u32;
107+
param.extend_from_slice(&[0xff; 10]);
108+
param.extend_from_slice(b"suffix");
109+
param.extend_from_slice(&offset.to_le_bytes());
110+
111+
let substituted = substitute_raw_versionstamp(param.clone(), &versionstamp).unwrap();
112+
113+
assert_eq!(substituted.len(), param.len() - 4);
114+
assert_eq!(&substituted[..offset as usize], b"prefix");
115+
assert_eq!(
116+
&substituted[offset as usize..offset as usize + 10],
117+
&versionstamp.as_bytes()[..10]
118+
);
119+
assert_eq!(&substituted[offset as usize + 10..], b"suffix");
120+
}
121+
122+
#[test]
123+
fn test_substitute_raw_versionstamp_matches_fdb_metadata_value_operand() {
124+
let versionstamp = generate_versionstamp(100);
125+
let mut param = vec![0; 14];
126+
param[10..].copy_from_slice(&0u32.to_le_bytes());
127+
128+
let substituted = substitute_raw_versionstamp(param, &versionstamp).unwrap();
129+
130+
assert_eq!(substituted, versionstamp.as_bytes()[..10]);
131+
}
132+
133+
#[test]
134+
fn test_substitute_raw_versionstamp_preserves_depot_suffix_bytes() {
135+
let versionstamp = generate_versionstamp(100);
136+
let mut param = vec![0xff; 10];
137+
param.extend_from_slice(&[0; 6]);
138+
param.extend_from_slice(&0u32.to_le_bytes());
139+
140+
let substituted = substitute_raw_versionstamp(param, &versionstamp).unwrap();
141+
142+
assert_eq!(substituted.len(), 16);
143+
assert_eq!(&substituted[..10], &versionstamp.as_bytes()[..10]);
144+
assert_eq!(&substituted[10..], &[0; 6]);
145+
}
146+
147+
#[test]
148+
fn test_substitute_versionstamp_matches_official_tuple_operand_layout() {
149+
let tuple = (
150+
"prefix",
151+
Versionstamp::incomplete(12345),
152+
"suffix",
153+
);
154+
let mut ours = pack_with_versionstamp(&tuple);
155+
let official = ours.clone();
156+
let versionstamp = generate_versionstamp(54321);
157+
158+
substitute_versionstamp(&mut ours, versionstamp.clone()).unwrap();
159+
160+
let offset_start = official.len() - 4;
161+
let offset = u32::from_le_bytes(
162+
official[offset_start..]
163+
.try_into()
164+
.expect("official tuple offset should be four bytes"),
165+
) as usize;
166+
let mut expected = official[..offset_start].to_vec();
167+
expected[offset..offset + 10].copy_from_slice(&versionstamp.as_bytes()[..10]);
168+
169+
assert_eq!(ours, expected);
170+
171+
let unpacked: (String, Versionstamp, String) = unpack(&ours).unwrap();
172+
assert!(unpacked.1.is_complete());
173+
assert_eq!(unpacked.1.user_version(), 12345);
174+
}
175+
102176
#[test]
103177
fn test_pack_and_substitute_versionstamp() {
104-
let incomplete = Versionstamp::from([0xff; 12]);
178+
let incomplete = Versionstamp::incomplete(100);
105179
let tuple = vec![
106180
Element::String("mykey".into()),
107181
Element::Versionstamp(incomplete),
@@ -155,8 +229,6 @@ fn test_versionstamp_incomplete_preserves_user_version() {
155229
"User version bytes should match"
156230
);
157231

158-
// Test with substitute_versionstamp - the user version from the incomplete versionstamp
159-
// should be ignored and replaced with the one from the generated versionstamp
160232
let tuple = vec![
161233
Element::String("test".into()),
162234
Element::Versionstamp(incomplete),
@@ -172,11 +244,10 @@ fn test_versionstamp_incomplete_preserves_user_version() {
172244
match &unpacked[1] {
173245
Element::Versionstamp(v) => {
174246
assert!(v.is_complete());
175-
// The user version should be from the generated versionstamp, not the original incomplete one
176247
assert_eq!(
177248
v.user_version(),
178-
new_user_version,
179-
"Substituted versionstamp should have the new user version"
249+
user_version,
250+
"Substituted versionstamp should preserve the tuple user version"
180251
);
181252
}
182253
_ => panic!("Expected versionstamp"),

0 commit comments

Comments
 (0)