mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 21:33:14 +00:00
968 lines
36 KiB
Rust
968 lines
36 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use byteorder::{ByteOrder, LittleEndian};
|
|
use std::fmt;
|
|
|
|
use crate::{Error, Result};
|
|
|
|
pub use crate::filemeta::{MrfOpKind, MrfReplicateEntry};
|
|
|
|
pub const MRF_META_FORMAT: u16 = 1;
|
|
pub const MRF_META_VERSION: u16 = 1;
|
|
|
|
const MRF_ENVELOPE_MAGIC: [u8; 4] = *b"MRFE";
|
|
const MRF_ENVELOPE_HEADER_LEN: usize = 24;
|
|
pub const MRF_ENVELOPE_FORMAT: u16 = 1;
|
|
pub const MRF_ENVELOPE_VERSION: u16 = 1;
|
|
|
|
const CAPABILITY_OPERATION_KIND: u64 = 1 << 0;
|
|
const CAPABILITY_TARGET_ARNS: u64 = 1 << 1;
|
|
const CAPABILITY_FORCE_DELETE: u64 = 1 << 2;
|
|
const CAPABILITY_DELETE_MARKER_MTIME: u64 = 1 << 3;
|
|
const MRF_KNOWN_CAPABILITIES: u64 =
|
|
CAPABILITY_OPERATION_KIND | CAPABILITY_TARGET_ARNS | CAPABILITY_FORCE_DELETE | CAPABILITY_DELETE_MARKER_MTIME;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MrfCapability {
|
|
OperationKind,
|
|
TargetArns,
|
|
ForceDelete,
|
|
DeleteMarkerMtime,
|
|
}
|
|
|
|
impl MrfCapability {
|
|
const fn bit(self) -> u64 {
|
|
match self {
|
|
Self::OperationKind => CAPABILITY_OPERATION_KIND,
|
|
Self::TargetArns => CAPABILITY_TARGET_ARNS,
|
|
Self::ForceDelete => CAPABILITY_FORCE_DELETE,
|
|
Self::DeleteMarkerMtime => CAPABILITY_DELETE_MARKER_MTIME,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct MrfCapabilities(u64);
|
|
|
|
impl MrfCapabilities {
|
|
pub const fn current() -> Self {
|
|
Self(MRF_KNOWN_CAPABILITIES)
|
|
}
|
|
|
|
pub const fn empty() -> Self {
|
|
Self(0)
|
|
}
|
|
|
|
pub const fn with(capability: MrfCapability) -> Self {
|
|
Self(capability.bit())
|
|
}
|
|
|
|
pub const fn bits(self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
pub const fn contains(self, capability: MrfCapability) -> bool {
|
|
self.0 & capability.bit() != 0
|
|
}
|
|
|
|
pub const fn supports(self, required: Self) -> bool {
|
|
self.0 & required.0 == required.0
|
|
}
|
|
|
|
pub fn from_bits(bits: u64) -> std::result::Result<Self, MrfEnvelopeError> {
|
|
let unknown = bits & !MRF_KNOWN_CAPABILITIES;
|
|
if unknown != 0 {
|
|
return Err(MrfEnvelopeError::UnknownCapabilities { bits: unknown });
|
|
}
|
|
Ok(Self(bits))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct MrfProtocolCapabilities {
|
|
version: u16,
|
|
min_reader_version: u16,
|
|
capabilities: MrfCapabilities,
|
|
}
|
|
|
|
impl MrfProtocolCapabilities {
|
|
pub const fn new(version: u16, min_reader_version: u16, capabilities: MrfCapabilities) -> Self {
|
|
Self {
|
|
version,
|
|
min_reader_version,
|
|
capabilities,
|
|
}
|
|
}
|
|
|
|
pub const fn current() -> Self {
|
|
Self {
|
|
version: MRF_ENVELOPE_VERSION,
|
|
min_reader_version: MRF_ENVELOPE_VERSION,
|
|
capabilities: MrfCapabilities::current(),
|
|
}
|
|
}
|
|
|
|
pub const fn version(self) -> u16 {
|
|
self.version
|
|
}
|
|
|
|
pub const fn min_reader_version(self) -> u16 {
|
|
self.min_reader_version
|
|
}
|
|
|
|
pub const fn capabilities(self) -> MrfCapabilities {
|
|
self.capabilities
|
|
}
|
|
|
|
pub fn negotiate(self, peer: Self) -> std::result::Result<Self, MrfEnvelopeError> {
|
|
MrfCapabilities::from_bits(self.capabilities.bits())?;
|
|
MrfCapabilities::from_bits(peer.capabilities.bits())?;
|
|
if self.min_reader_version > self.version {
|
|
return Err(MrfEnvelopeError::InvalidVersionRange {
|
|
version: self.version,
|
|
min_reader_version: self.min_reader_version,
|
|
});
|
|
}
|
|
if peer.min_reader_version > peer.version {
|
|
return Err(MrfEnvelopeError::InvalidVersionRange {
|
|
version: peer.version,
|
|
min_reader_version: peer.min_reader_version,
|
|
});
|
|
}
|
|
if self.version < MRF_ENVELOPE_VERSION {
|
|
return Err(MrfEnvelopeError::UnsupportedVersion { version: self.version });
|
|
}
|
|
if peer.version < MRF_ENVELOPE_VERSION {
|
|
return Err(MrfEnvelopeError::UnsupportedVersion { version: peer.version });
|
|
}
|
|
if peer.min_reader_version > self.version {
|
|
return Err(MrfEnvelopeError::RollbackFenced {
|
|
min_reader_version: peer.min_reader_version,
|
|
supported_version: self.version,
|
|
});
|
|
}
|
|
if self.min_reader_version > peer.version {
|
|
return Err(MrfEnvelopeError::RollbackFenced {
|
|
min_reader_version: self.min_reader_version,
|
|
supported_version: peer.version,
|
|
});
|
|
}
|
|
Ok(Self {
|
|
version: self.version.min(peer.version),
|
|
min_reader_version: self.min_reader_version.max(peer.min_reader_version),
|
|
capabilities: MrfCapabilities(self.capabilities.bits() & peer.capabilities.bits()),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct MrfEnvelope {
|
|
protocol: MrfProtocolCapabilities,
|
|
payload: Vec<u8>,
|
|
}
|
|
|
|
impl MrfEnvelope {
|
|
pub fn new(protocol: MrfProtocolCapabilities, payload: Vec<u8>) -> std::result::Result<Self, MrfEnvelopeError> {
|
|
if protocol.version != MRF_ENVELOPE_VERSION {
|
|
return Err(MrfEnvelopeError::UnsupportedVersion {
|
|
version: protocol.version,
|
|
});
|
|
}
|
|
if protocol.min_reader_version > protocol.version {
|
|
return Err(MrfEnvelopeError::InvalidVersionRange {
|
|
version: protocol.version,
|
|
min_reader_version: protocol.min_reader_version,
|
|
});
|
|
}
|
|
MrfCapabilities::from_bits(protocol.capabilities.bits())?;
|
|
Ok(Self { protocol, payload })
|
|
}
|
|
|
|
pub const fn protocol(&self) -> MrfProtocolCapabilities {
|
|
self.protocol
|
|
}
|
|
|
|
pub fn payload(&self) -> &[u8] {
|
|
&self.payload
|
|
}
|
|
|
|
pub fn encode(&self) -> std::result::Result<Vec<u8>, MrfEnvelopeError> {
|
|
let payload_len: u32 = self.payload.len().try_into().map_err(|_| MrfEnvelopeError::PayloadTooLarge)?;
|
|
let mut data = Vec::with_capacity(MRF_ENVELOPE_HEADER_LEN + self.payload.len());
|
|
data.extend_from_slice(&MRF_ENVELOPE_MAGIC);
|
|
data.extend_from_slice(&MRF_ENVELOPE_FORMAT.to_le_bytes());
|
|
data.extend_from_slice(&self.protocol.version.to_le_bytes());
|
|
data.extend_from_slice(&self.protocol.min_reader_version.to_le_bytes());
|
|
data.extend_from_slice(&0u16.to_le_bytes());
|
|
data.extend_from_slice(&self.protocol.capabilities.bits().to_le_bytes());
|
|
data.extend_from_slice(&payload_len.to_le_bytes());
|
|
data.extend_from_slice(&self.payload);
|
|
Ok(data)
|
|
}
|
|
|
|
pub fn decode(data: &[u8], supported: MrfProtocolCapabilities) -> std::result::Result<Self, MrfEnvelopeError> {
|
|
if data.len() < MRF_ENVELOPE_HEADER_LEN {
|
|
return Err(MrfEnvelopeError::Truncated);
|
|
}
|
|
if data[..4] != MRF_ENVELOPE_MAGIC {
|
|
return Err(MrfEnvelopeError::InvalidMagic);
|
|
}
|
|
let format = LittleEndian::read_u16(&data[4..6]);
|
|
if format != MRF_ENVELOPE_FORMAT {
|
|
return Err(MrfEnvelopeError::UnsupportedFormat { format });
|
|
}
|
|
let version = LittleEndian::read_u16(&data[6..8]);
|
|
if version < MRF_ENVELOPE_VERSION {
|
|
return Err(MrfEnvelopeError::UnsupportedVersion { version });
|
|
}
|
|
let min_reader_version = LittleEndian::read_u16(&data[8..10]);
|
|
if min_reader_version > supported.version {
|
|
return Err(MrfEnvelopeError::RollbackFenced {
|
|
min_reader_version,
|
|
supported_version: supported.version,
|
|
});
|
|
}
|
|
if min_reader_version > version {
|
|
return Err(MrfEnvelopeError::InvalidVersionRange {
|
|
version,
|
|
min_reader_version,
|
|
});
|
|
}
|
|
let reserved = LittleEndian::read_u16(&data[10..12]);
|
|
if reserved != 0 {
|
|
return Err(MrfEnvelopeError::ReservedHeaderBits { bits: reserved });
|
|
}
|
|
let capabilities = MrfCapabilities::from_bits(LittleEndian::read_u64(&data[12..20]))?;
|
|
if !supported.capabilities.supports(capabilities) {
|
|
return Err(MrfEnvelopeError::MissingCapabilities {
|
|
required: capabilities.bits(),
|
|
available: supported.capabilities.bits(),
|
|
});
|
|
}
|
|
let payload_len = LittleEndian::read_u32(&data[20..24]);
|
|
let actual_len = data.len() - MRF_ENVELOPE_HEADER_LEN;
|
|
if usize::try_from(payload_len).map_err(|_| MrfEnvelopeError::PayloadTooLarge)? != actual_len {
|
|
return Err(MrfEnvelopeError::PayloadLengthMismatch {
|
|
declared: payload_len,
|
|
actual: actual_len,
|
|
});
|
|
}
|
|
Ok(Self {
|
|
protocol: MrfProtocolCapabilities {
|
|
version,
|
|
min_reader_version,
|
|
capabilities,
|
|
},
|
|
payload: data[MRF_ENVELOPE_HEADER_LEN..].to_vec(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum MrfEnvelopeError {
|
|
Truncated,
|
|
InvalidMagic,
|
|
UnsupportedFormat {
|
|
format: u16,
|
|
},
|
|
UnsupportedVersion {
|
|
version: u16,
|
|
},
|
|
InvalidVersionRange {
|
|
version: u16,
|
|
min_reader_version: u16,
|
|
},
|
|
RollbackFenced {
|
|
min_reader_version: u16,
|
|
supported_version: u16,
|
|
},
|
|
ReservedHeaderBits {
|
|
bits: u16,
|
|
},
|
|
UnknownCapabilities {
|
|
bits: u64,
|
|
},
|
|
MissingCapabilities {
|
|
required: u64,
|
|
available: u64,
|
|
},
|
|
PayloadLengthMismatch {
|
|
declared: u32,
|
|
actual: usize,
|
|
},
|
|
PayloadTooLarge,
|
|
}
|
|
|
|
impl fmt::Display for MrfEnvelopeError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Truncated => write!(f, "truncated MRF envelope"),
|
|
Self::InvalidMagic => write!(f, "invalid MRF envelope magic"),
|
|
Self::UnsupportedFormat { format } => write!(f, "unsupported MRF envelope format {format}"),
|
|
Self::UnsupportedVersion { version } => write!(f, "unsupported MRF envelope version {version}"),
|
|
Self::InvalidVersionRange {
|
|
version,
|
|
min_reader_version,
|
|
} => {
|
|
write!(f, "invalid MRF version range: version {version}, minimum reader {min_reader_version}")
|
|
}
|
|
Self::RollbackFenced {
|
|
min_reader_version,
|
|
supported_version,
|
|
} => write!(
|
|
f,
|
|
"MRF rollback fenced: reader version {supported_version} is below required {min_reader_version}"
|
|
),
|
|
Self::ReservedHeaderBits { bits } => write!(f, "reserved MRF envelope header bits are set: 0x{bits:04x}"),
|
|
Self::UnknownCapabilities { bits } => write!(f, "unknown MRF capability bits 0x{bits:016x}"),
|
|
Self::MissingCapabilities { required, available } => {
|
|
write!(f, "MRF capabilities 0x{available:016x} do not satisfy required 0x{required:016x}")
|
|
}
|
|
Self::PayloadLengthMismatch { declared, actual } => {
|
|
write!(f, "MRF payload length is {actual}, expected {declared}")
|
|
}
|
|
Self::PayloadTooLarge => write!(f, "MRF payload exceeds the envelope length limit"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for MrfEnvelopeError {}
|
|
|
|
pub const MRF_V2_NAMESPACE: &str = "config/replication-v2";
|
|
pub const MRF_V2_FILE: &str = "config/replication-v2/mrf.bin";
|
|
pub const MRF_V2_FORMAT: u16 = 2;
|
|
pub const MRF_V2_VERSION: u16 = 2;
|
|
|
|
const MRF_V2_MAGIC: [u8; 4] = *b"MRF2";
|
|
const MRF_V2_HEADER_LEN: usize = 24;
|
|
const MRF_V2_KNOWN_CAPABILITIES: u64 = 0b1111;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum MrfV2Error {
|
|
InvalidMagic,
|
|
Truncated,
|
|
UnsupportedNamespace { namespace: String },
|
|
UnsupportedFormat { format: u16 },
|
|
UnsupportedVersion { version: u16 },
|
|
InvalidVersionRange { version: u16, min_reader_version: u16 },
|
|
RollbackFenced { min_reader_version: u16, reader_version: u16 },
|
|
ReservedHeaderBits { bits: u16 },
|
|
UnknownCapabilities { bits: u64 },
|
|
MissingCapabilities { required: u64, available: u64 },
|
|
PayloadLengthMismatch { declared: u32, actual: usize },
|
|
WriterEnabled,
|
|
}
|
|
|
|
impl std::fmt::Display for MrfV2Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::InvalidMagic => write!(f, "invalid MRF v2 magic"),
|
|
Self::Truncated => write!(f, "truncated MRF v2 envelope"),
|
|
Self::UnsupportedNamespace { namespace } => write!(f, "unsupported MRF v2 namespace {namespace}"),
|
|
Self::UnsupportedFormat { format } => write!(f, "unsupported MRF v2 format {format}"),
|
|
Self::UnsupportedVersion { version } => write!(f, "unsupported MRF v2 version {version}"),
|
|
Self::InvalidVersionRange {
|
|
version,
|
|
min_reader_version,
|
|
} => {
|
|
write!(f, "invalid MRF v2 version range: version {version}, minimum reader {min_reader_version}")
|
|
}
|
|
Self::RollbackFenced {
|
|
min_reader_version,
|
|
reader_version,
|
|
} => write!(
|
|
f,
|
|
"MRF v2 rollback fenced: reader version {reader_version} is below required {min_reader_version}"
|
|
),
|
|
Self::ReservedHeaderBits { bits } => write!(f, "reserved MRF v2 header bits are set: 0x{bits:04x}"),
|
|
Self::UnknownCapabilities { bits } => write!(f, "unknown MRF v2 capability bits 0x{bits:016x}"),
|
|
Self::MissingCapabilities { required, available } => {
|
|
write!(f, "MRF v2 capabilities 0x{available:016x} do not satisfy required 0x{required:016x}")
|
|
}
|
|
Self::PayloadLengthMismatch { declared, actual } => {
|
|
write!(f, "MRF v2 payload is {actual} bytes, expected {declared}")
|
|
}
|
|
Self::WriterEnabled => write!(f, "MRF v2 writer must remain dormant"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for MrfV2Error {}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct MrfV2Capabilities(u64);
|
|
|
|
impl MrfV2Capabilities {
|
|
pub const fn current() -> Self {
|
|
Self(MRF_V2_KNOWN_CAPABILITIES)
|
|
}
|
|
|
|
pub const fn empty() -> Self {
|
|
Self(0)
|
|
}
|
|
|
|
pub const fn bits(self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
pub fn from_bits(bits: u64) -> std::result::Result<Self, MrfV2Error> {
|
|
let unknown = bits & !MRF_V2_KNOWN_CAPABILITIES;
|
|
if unknown != 0 {
|
|
return Err(MrfV2Error::UnknownCapabilities { bits: unknown });
|
|
}
|
|
Ok(Self(bits))
|
|
}
|
|
|
|
pub const fn supports(self, required: Self) -> bool {
|
|
self.0 & required.0 == required.0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct MrfV2Readiness {
|
|
reader_version: u16,
|
|
capabilities: MrfV2Capabilities,
|
|
writer_enabled: bool,
|
|
}
|
|
|
|
impl MrfV2Readiness {
|
|
pub const fn dormant() -> Self {
|
|
Self {
|
|
reader_version: MRF_V2_VERSION,
|
|
capabilities: MrfV2Capabilities::current(),
|
|
writer_enabled: false,
|
|
}
|
|
}
|
|
|
|
pub const fn new(reader_version: u16, capabilities: MrfV2Capabilities, writer_enabled: bool) -> Self {
|
|
Self {
|
|
reader_version,
|
|
capabilities,
|
|
writer_enabled,
|
|
}
|
|
}
|
|
|
|
pub const fn writer_enabled(self) -> bool {
|
|
self.writer_enabled
|
|
}
|
|
|
|
pub fn reader(self) -> std::result::Result<MrfV2Reader, MrfV2Error> {
|
|
if self.writer_enabled {
|
|
return Err(MrfV2Error::WriterEnabled);
|
|
}
|
|
if self.reader_version != MRF_V2_VERSION {
|
|
return Err(MrfV2Error::UnsupportedVersion {
|
|
version: self.reader_version,
|
|
});
|
|
}
|
|
MrfV2Capabilities::from_bits(self.capabilities.bits())?;
|
|
Ok(MrfV2Reader { readiness: self })
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct MrfV2Reader {
|
|
readiness: MrfV2Readiness,
|
|
}
|
|
|
|
impl MrfV2Reader {
|
|
pub fn read(self, namespace: &str, data: &[u8]) -> std::result::Result<MrfV2Envelope, MrfV2Error> {
|
|
if namespace != MRF_V2_NAMESPACE {
|
|
return Err(MrfV2Error::UnsupportedNamespace {
|
|
namespace: namespace.to_string(),
|
|
});
|
|
}
|
|
if data.len() < MRF_V2_HEADER_LEN {
|
|
return Err(MrfV2Error::Truncated);
|
|
}
|
|
if data[..4] != MRF_V2_MAGIC {
|
|
return Err(MrfV2Error::InvalidMagic);
|
|
}
|
|
let format = LittleEndian::read_u16(&data[4..6]);
|
|
if format != MRF_V2_FORMAT {
|
|
return Err(MrfV2Error::UnsupportedFormat { format });
|
|
}
|
|
let version = LittleEndian::read_u16(&data[6..8]);
|
|
if version != MRF_V2_VERSION || version != self.readiness.reader_version {
|
|
return Err(MrfV2Error::UnsupportedVersion { version });
|
|
}
|
|
let min_reader_version = LittleEndian::read_u16(&data[8..10]);
|
|
if min_reader_version > self.readiness.reader_version {
|
|
return Err(MrfV2Error::RollbackFenced {
|
|
min_reader_version,
|
|
reader_version: self.readiness.reader_version,
|
|
});
|
|
}
|
|
if min_reader_version > version {
|
|
return Err(MrfV2Error::InvalidVersionRange {
|
|
version,
|
|
min_reader_version,
|
|
});
|
|
}
|
|
let reserved = LittleEndian::read_u16(&data[10..12]);
|
|
if reserved != 0 {
|
|
return Err(MrfV2Error::ReservedHeaderBits { bits: reserved });
|
|
}
|
|
let capabilities = MrfV2Capabilities::from_bits(LittleEndian::read_u64(&data[12..20]))?;
|
|
if !self.readiness.capabilities.supports(capabilities) {
|
|
return Err(MrfV2Error::MissingCapabilities {
|
|
required: capabilities.bits(),
|
|
available: self.readiness.capabilities.bits(),
|
|
});
|
|
}
|
|
let payload_len = LittleEndian::read_u32(&data[20..24]);
|
|
let actual = data.len() - MRF_V2_HEADER_LEN;
|
|
if usize::try_from(payload_len).map_err(|_| MrfV2Error::PayloadLengthMismatch {
|
|
declared: payload_len,
|
|
actual,
|
|
})? != actual
|
|
{
|
|
return Err(MrfV2Error::PayloadLengthMismatch {
|
|
declared: payload_len,
|
|
actual,
|
|
});
|
|
}
|
|
Ok(MrfV2Envelope {
|
|
version,
|
|
min_reader_version,
|
|
capabilities,
|
|
payload: data[MRF_V2_HEADER_LEN..].to_vec(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct MrfV2Envelope {
|
|
version: u16,
|
|
min_reader_version: u16,
|
|
capabilities: MrfV2Capabilities,
|
|
payload: Vec<u8>,
|
|
}
|
|
|
|
impl MrfV2Envelope {
|
|
pub const fn version(&self) -> u16 {
|
|
self.version
|
|
}
|
|
|
|
pub const fn min_reader_version(&self) -> u16 {
|
|
self.min_reader_version
|
|
}
|
|
|
|
pub const fn capabilities(&self) -> MrfV2Capabilities {
|
|
self.capabilities
|
|
}
|
|
|
|
pub fn payload(&self) -> &[u8] {
|
|
&self.payload
|
|
}
|
|
}
|
|
pub fn encode_mrf_file(entries: &[MrfReplicateEntry]) -> Result<Vec<u8>> {
|
|
let payload = rmp_serde::to_vec_named(entries).map_err(|e| Error::Other(e.to_string()))?;
|
|
let mut data = Vec::with_capacity(4 + payload.len());
|
|
let mut fmt = [0u8; 2];
|
|
LittleEndian::write_u16(&mut fmt, MRF_META_FORMAT);
|
|
data.extend_from_slice(&fmt);
|
|
let mut ver = [0u8; 2];
|
|
LittleEndian::write_u16(&mut ver, MRF_META_VERSION);
|
|
data.extend_from_slice(&ver);
|
|
data.extend_from_slice(&payload);
|
|
Ok(data)
|
|
}
|
|
|
|
pub fn decode_mrf_file(data: &[u8]) -> Result<Vec<MrfReplicateEntry>> {
|
|
if data.len() <= 4 {
|
|
return Err(Error::CorruptedFormat);
|
|
}
|
|
let mut fmt = [0u8; 2];
|
|
fmt.copy_from_slice(&data[0..2]);
|
|
if LittleEndian::read_u16(&fmt) != MRF_META_FORMAT {
|
|
return Err(Error::CorruptedFormat);
|
|
}
|
|
let mut ver = [0u8; 2];
|
|
ver.copy_from_slice(&data[2..4]);
|
|
if LittleEndian::read_u16(&ver) != MRF_META_VERSION {
|
|
return Err(Error::CorruptedFormat);
|
|
}
|
|
rmp_serde::from_slice(&data[4..]).map_err(|e| Error::Other(e.to_string()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use uuid::Uuid;
|
|
|
|
const ENVELOPE_FIXTURE: &[u8] = &[
|
|
b'M', b'R', b'F', b'E', 1, 0, 1, 0, 1, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 2, 3,
|
|
];
|
|
|
|
#[test]
|
|
fn mrf_file_round_trips_object_metadata_and_delete_entries() {
|
|
let obj_vid = Uuid::new_v4();
|
|
let del_vid = Uuid::new_v4();
|
|
let entries = vec![
|
|
MrfReplicateEntry {
|
|
bucket: "bucket-a".to_string(),
|
|
object: "metadata-a".to_string(),
|
|
version_id: Some(obj_vid),
|
|
retry_count: 1,
|
|
size: 1024,
|
|
op: MrfOpKind::Metadata,
|
|
force_delete_id: None,
|
|
force_delete_generation: None,
|
|
force_delete_local_commit: false,
|
|
force_delete: false,
|
|
delete_marker_version_id: None,
|
|
delete_marker: false,
|
|
delete_marker_mtime: None,
|
|
target_arns: vec!["arn:target-a".to_string()],
|
|
},
|
|
MrfReplicateEntry {
|
|
bucket: "bucket-a".to_string(),
|
|
object: "object-a".to_string(),
|
|
version_id: Some(obj_vid),
|
|
retry_count: 2,
|
|
size: 1024,
|
|
op: MrfOpKind::Object,
|
|
force_delete_id: None,
|
|
force_delete_generation: None,
|
|
force_delete_local_commit: false,
|
|
force_delete: false,
|
|
delete_marker_version_id: None,
|
|
delete_marker: false,
|
|
delete_marker_mtime: None,
|
|
target_arns: vec!["arn:target-a".to_string(), "arn:target-b".to_string()],
|
|
},
|
|
MrfReplicateEntry {
|
|
bucket: "bucket-a".to_string(),
|
|
object: "delete-a".to_string(),
|
|
version_id: None,
|
|
retry_count: 0,
|
|
size: 0,
|
|
op: MrfOpKind::Delete,
|
|
force_delete_id: None,
|
|
force_delete_generation: None,
|
|
force_delete_local_commit: false,
|
|
force_delete: true,
|
|
delete_marker_version_id: Some(del_vid),
|
|
delete_marker: true,
|
|
delete_marker_mtime: Some(1_705_312_200_123_456_789),
|
|
target_arns: vec!["arn:target-a".to_string()],
|
|
},
|
|
];
|
|
|
|
let encoded = encode_mrf_file(&entries).expect("mrf file should encode");
|
|
let decoded = decode_mrf_file(&encoded).expect("mrf file should decode");
|
|
|
|
assert_eq!(decoded.len(), 3);
|
|
assert_eq!(decoded[0].version_id, Some(obj_vid));
|
|
assert_eq!(decoded[0].op, MrfOpKind::Metadata);
|
|
assert_eq!(decoded[0].target_arns, vec!["arn:target-a".to_string()]);
|
|
assert_eq!(decoded[0].delete_marker_mtime, None);
|
|
assert!(!decoded[0].force_delete);
|
|
assert_eq!(decoded[1].op, MrfOpKind::Object);
|
|
assert!(!decoded[1].force_delete);
|
|
assert_eq!(decoded[1].target_arns, vec!["arn:target-a".to_string(), "arn:target-b".to_string()]);
|
|
assert_eq!(decoded[1].delete_marker_mtime, None);
|
|
assert_eq!(decoded[2].delete_marker_version_id, Some(del_vid));
|
|
assert_eq!(decoded[2].op, MrfOpKind::Delete);
|
|
assert!(decoded[2].force_delete);
|
|
assert_eq!(decoded[2].target_arns, vec!["arn:target-a".to_string()]);
|
|
assert!(decoded[2].delete_marker);
|
|
assert_eq!(
|
|
decoded[2].delete_marker_mtime,
|
|
Some(1_705_312_200_123_456_789),
|
|
"delete-marker mtime must survive the MRF disk round-trip"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mrf_legacy_file_without_op_decodes_as_object() {
|
|
let mut payload = Vec::new();
|
|
rmp::encode::write_array_len(&mut payload, 1).expect("array len should encode");
|
|
rmp::encode::write_map_len(&mut payload, 4).expect("map len should encode");
|
|
rmp::encode::write_str(&mut payload, "bucket").expect("bucket key should encode");
|
|
rmp::encode::write_str(&mut payload, "old-bucket").expect("bucket value should encode");
|
|
rmp::encode::write_str(&mut payload, "object").expect("object key should encode");
|
|
rmp::encode::write_str(&mut payload, "old-key").expect("object value should encode");
|
|
rmp::encode::write_str(&mut payload, "retryCount").expect("retry key should encode");
|
|
rmp::encode::write_i32(&mut payload, 2).expect("retry value should encode");
|
|
rmp::encode::write_str(&mut payload, "size").expect("size key should encode");
|
|
rmp::encode::write_i64(&mut payload, 100).expect("size value should encode");
|
|
|
|
let mut data = Vec::with_capacity(4 + payload.len());
|
|
data.extend_from_slice(&MRF_META_FORMAT.to_le_bytes());
|
|
data.extend_from_slice(&MRF_META_VERSION.to_le_bytes());
|
|
data.extend_from_slice(&payload);
|
|
|
|
let decoded = decode_mrf_file(&data).expect("legacy mrf file should decode");
|
|
|
|
assert_eq!(decoded.len(), 1);
|
|
assert_eq!(decoded[0].bucket, "old-bucket");
|
|
assert_eq!(decoded[0].object, "old-key");
|
|
assert_eq!(decoded[0].retry_count, 2);
|
|
assert_eq!(decoded[0].size, 100);
|
|
assert_eq!(decoded[0].op, MrfOpKind::Object);
|
|
assert!(decoded[0].target_arns.is_empty());
|
|
assert!(!decoded[0].force_delete);
|
|
// Old files lack the deleteMarkerMtime key; it must default to None so replay keeps the
|
|
// pre-#867 fallback to the current time.
|
|
assert_eq!(decoded[0].delete_marker_mtime, None);
|
|
}
|
|
|
|
#[test]
|
|
fn mrf_file_defaults_missing_force_delete_to_false() {
|
|
let mut payload = Vec::new();
|
|
rmp::encode::write_array_len(&mut payload, 1).expect("array len should encode");
|
|
rmp::encode::write_map_len(&mut payload, 5).expect("map len should encode");
|
|
rmp::encode::write_str(&mut payload, "bucket").expect("bucket key should encode");
|
|
rmp::encode::write_str(&mut payload, "old-bucket").expect("bucket value should encode");
|
|
rmp::encode::write_str(&mut payload, "object").expect("object key should encode");
|
|
rmp::encode::write_str(&mut payload, "old-key").expect("object value should encode");
|
|
rmp::encode::write_str(&mut payload, "retryCount").expect("retry key should encode");
|
|
rmp::encode::write_i32(&mut payload, 1).expect("retry value should encode");
|
|
rmp::encode::write_str(&mut payload, "size").expect("size key should encode");
|
|
rmp::encode::write_i64(&mut payload, 0).expect("size value should encode");
|
|
rmp::encode::write_str(&mut payload, "op").expect("op key should encode");
|
|
rmp::encode::write_str(&mut payload, "delete").expect("op value should encode");
|
|
|
|
let mut data = Vec::with_capacity(4 + payload.len());
|
|
data.extend_from_slice(&MRF_META_FORMAT.to_le_bytes());
|
|
data.extend_from_slice(&MRF_META_VERSION.to_le_bytes());
|
|
data.extend_from_slice(&payload);
|
|
|
|
let decoded = decode_mrf_file(&data).expect("MRF payload should decode");
|
|
|
|
assert_eq!(decoded[0].op, MrfOpKind::Delete);
|
|
assert!(!decoded[0].force_delete);
|
|
}
|
|
|
|
#[test]
|
|
fn mrf_file_rejects_invalid_header() {
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&2u16.to_le_bytes());
|
|
data.extend_from_slice(&MRF_META_VERSION.to_le_bytes());
|
|
data.push(0x90);
|
|
|
|
assert!(matches!(decode_mrf_file(&data), Err(Error::CorruptedFormat)));
|
|
}
|
|
|
|
#[test]
|
|
fn envelope_fixture_is_stable_and_round_trips() {
|
|
let envelope =
|
|
MrfEnvelope::new(MrfProtocolCapabilities::current(), vec![1, 2, 3]).expect("current MRF envelope should be valid");
|
|
assert_eq!(envelope.encode().expect("envelope should encode"), ENVELOPE_FIXTURE);
|
|
let decoded = MrfEnvelope::decode(ENVELOPE_FIXTURE, MrfProtocolCapabilities::current()).expect("fixture should decode");
|
|
assert_eq!(decoded.protocol(), MrfProtocolCapabilities::current());
|
|
assert_eq!(decoded.payload(), &[1, 2, 3]);
|
|
}
|
|
|
|
#[test]
|
|
fn envelope_accepts_a_forward_compatible_writer_version() {
|
|
let mut version = ENVELOPE_FIXTURE.to_vec();
|
|
version[6..8].copy_from_slice(&2u16.to_le_bytes());
|
|
let decoded =
|
|
MrfEnvelope::decode(&version, MrfProtocolCapabilities::current()).expect("compatible v2 envelope should decode");
|
|
assert_eq!(decoded.protocol().version(), 2);
|
|
assert_eq!(decoded.protocol().min_reader_version(), 1);
|
|
|
|
let mut fenced = version;
|
|
fenced[8..10].copy_from_slice(&2u16.to_le_bytes());
|
|
assert_eq!(
|
|
MrfEnvelope::decode(&fenced, MrfProtocolCapabilities::current()),
|
|
Err(MrfEnvelopeError::RollbackFenced {
|
|
min_reader_version: 2,
|
|
supported_version: 1,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn envelope_rejects_unknown_capability_bits() {
|
|
let mut capabilities = ENVELOPE_FIXTURE.to_vec();
|
|
capabilities[12..20].copy_from_slice(&(1u64 << 63).to_le_bytes());
|
|
|
|
assert_eq!(
|
|
MrfEnvelope::decode(&capabilities, MrfProtocolCapabilities::current()),
|
|
Err(MrfEnvelopeError::UnknownCapabilities { bits: 1u64 << 63 })
|
|
);
|
|
|
|
let mut reserved = ENVELOPE_FIXTURE.to_vec();
|
|
reserved[10..12].copy_from_slice(&1u16.to_le_bytes());
|
|
assert_eq!(
|
|
MrfEnvelope::decode(&reserved, MrfProtocolCapabilities::current()),
|
|
Err(MrfEnvelopeError::ReservedHeaderBits { bits: 1 })
|
|
);
|
|
|
|
let mut legacy = ENVELOPE_FIXTURE.to_vec();
|
|
legacy[6..8].copy_from_slice(&0u16.to_le_bytes());
|
|
assert_eq!(
|
|
MrfEnvelope::decode(&legacy, MrfProtocolCapabilities::current()),
|
|
Err(MrfEnvelopeError::UnsupportedVersion { version: 0 })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn envelope_rejects_rollback_and_missing_capabilities() {
|
|
let mut rollback = ENVELOPE_FIXTURE.to_vec();
|
|
rollback[8..10].copy_from_slice(&2u16.to_le_bytes());
|
|
assert_eq!(
|
|
MrfEnvelope::decode(&rollback, MrfProtocolCapabilities::current()),
|
|
Err(MrfEnvelopeError::RollbackFenced {
|
|
min_reader_version: 2,
|
|
supported_version: 1,
|
|
})
|
|
);
|
|
|
|
let required = MrfCapabilities::with(MrfCapability::TargetArns);
|
|
let envelope =
|
|
MrfEnvelope::new(MrfProtocolCapabilities::new(1, 1, required), Vec::new()).expect("known capability should be valid");
|
|
let encoded = envelope.encode().expect("envelope should encode");
|
|
assert_eq!(
|
|
MrfEnvelope::decode(&encoded, MrfProtocolCapabilities::new(1, 1, MrfCapabilities::empty())),
|
|
Err(MrfEnvelopeError::MissingCapabilities {
|
|
required: required.bits(),
|
|
available: 0,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_negotiation_fences_rollback() {
|
|
let current = MrfProtocolCapabilities::current();
|
|
let rollback = MrfProtocolCapabilities::new(1, 2, MrfCapabilities::current());
|
|
assert_eq!(
|
|
current.negotiate(rollback),
|
|
Err(MrfEnvelopeError::InvalidVersionRange {
|
|
version: 1,
|
|
min_reader_version: 2,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_negotiation_rejects_invalid_local_version_range() {
|
|
let invalid = MrfProtocolCapabilities::new(1, 2, MrfCapabilities::current());
|
|
assert_eq!(
|
|
invalid.negotiate(MrfProtocolCapabilities::current()),
|
|
Err(MrfEnvelopeError::InvalidVersionRange {
|
|
version: 1,
|
|
min_reader_version: 2,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_negotiation_intersects_capabilities() {
|
|
let local = MrfProtocolCapabilities::new(1, 1, MrfCapabilities::with(MrfCapability::TargetArns));
|
|
let peer = MrfProtocolCapabilities::new(1, 1, MrfCapabilities::with(MrfCapability::ForceDelete));
|
|
let negotiated = local.negotiate(peer).expect("same-version peers should negotiate");
|
|
assert_eq!(negotiated.capabilities(), MrfCapabilities::empty());
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_negotiation_accepts_a_forward_compatible_peer() {
|
|
let reader = MrfProtocolCapabilities::current();
|
|
let writer = MrfProtocolCapabilities::new(2, 1, MrfCapabilities::current());
|
|
let negotiated = reader
|
|
.negotiate(writer)
|
|
.expect("v1 reader should negotiate with a compatible v2 writer");
|
|
assert_eq!(negotiated.version(), 1);
|
|
assert_eq!(negotiated.min_reader_version(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn v1_file_contract_and_v2_namespace_remain_separate() {
|
|
assert_eq!(encode_mrf_file(&[]).expect("v1 empty file should encode"), vec![1, 0, 1, 0, 0x90]);
|
|
assert_eq!(MRF_V2_FILE, "config/replication-v2/mrf.bin");
|
|
assert_ne!(MRF_V2_FILE, "config/replication/mrf.bin");
|
|
}
|
|
|
|
#[test]
|
|
fn dormant_v2_reader_accepts_stable_fixture() {
|
|
let fixture = [
|
|
b'M', b'R', b'F', b'2', 2, 0, 2, 0, 2, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 2, 3,
|
|
];
|
|
let readiness = MrfV2Readiness::dormant();
|
|
assert!(!readiness.writer_enabled());
|
|
let envelope = readiness
|
|
.reader()
|
|
.expect("dormant readiness should expose the reader")
|
|
.read(MRF_V2_NAMESPACE, &fixture)
|
|
.expect("v2 fixture should decode");
|
|
assert_eq!(envelope.version(), MRF_V2_VERSION);
|
|
assert_eq!(envelope.min_reader_version(), MRF_V2_VERSION);
|
|
assert_eq!(envelope.capabilities(), MrfV2Capabilities::current());
|
|
assert_eq!(envelope.payload(), &[1, 2, 3]);
|
|
}
|
|
|
|
#[test]
|
|
fn v2_reader_rejects_wrong_namespace_version_capability_and_rollback() {
|
|
let fixture = [
|
|
b'M', b'R', b'F', b'2', 2, 0, 2, 0, 2, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 2, 3,
|
|
];
|
|
let reader = MrfV2Readiness::dormant().reader().expect("dormant reader should initialize");
|
|
assert!(matches!(
|
|
reader.read("config/replication/mrf.bin", &fixture),
|
|
Err(MrfV2Error::UnsupportedNamespace { .. })
|
|
));
|
|
|
|
let mut version = fixture;
|
|
version[6..8].copy_from_slice(&1u16.to_le_bytes());
|
|
assert_eq!(
|
|
reader.read(MRF_V2_NAMESPACE, &version),
|
|
Err(MrfV2Error::UnsupportedVersion { version: 1 })
|
|
);
|
|
|
|
let mut capabilities = fixture;
|
|
capabilities[12..20].copy_from_slice(&(1u64 << 63).to_le_bytes());
|
|
assert_eq!(
|
|
reader.read(MRF_V2_NAMESPACE, &capabilities),
|
|
Err(MrfV2Error::UnknownCapabilities { bits: 1u64 << 63 })
|
|
);
|
|
|
|
let mut rollback = fixture;
|
|
rollback[8..10].copy_from_slice(&3u16.to_le_bytes());
|
|
assert_eq!(
|
|
reader.read(MRF_V2_NAMESPACE, &rollback),
|
|
Err(MrfV2Error::RollbackFenced {
|
|
min_reader_version: 3,
|
|
reader_version: 2,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn v2_readiness_rejects_writer_enablement_and_missing_capabilities() {
|
|
assert_eq!(
|
|
MrfV2Readiness::new(2, MrfV2Capabilities::current(), true).reader(),
|
|
Err(MrfV2Error::WriterEnabled)
|
|
);
|
|
|
|
let fixture = [
|
|
b'M', b'R', b'F', b'2', 2, 0, 2, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 2, 3,
|
|
];
|
|
let reader = MrfV2Readiness::new(2, MrfV2Capabilities::empty(), false)
|
|
.reader()
|
|
.expect("readiness with valid empty capabilities should initialize");
|
|
assert_eq!(
|
|
reader.read(MRF_V2_NAMESPACE, &fixture),
|
|
Err(MrfV2Error::MissingCapabilities {
|
|
required: 1,
|
|
available: 0,
|
|
})
|
|
);
|
|
}
|
|
}
|