use crate::record::{Lsn, RecordBuf, decode_body, encode_canonical};
use crate::wire::{DecodeError, DecodeResult, Reader};
pub const WAL_MAGIC: &[u8; 8] = b"FUSEEWAL";
pub const WAL_FORMAT: u32 = 3;
pub const BLOCK: u64 = 4096;
pub const WAL_HEADER_BYTES: u64 = BLOCK;
pub const BATCH_HEADER: usize = 56;
pub const SECTION_HEADER: usize = 24;
pub const MAX_BATCH: usize = 64 << 20;
pub const DAMAGE_WINDOW: usize = 1 << 20;
pub const FLAG_LIFE_START: u32 = 1;
pub use crate::wire::xxh64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WalError {
BadHeader,
UnsupportedFormat(u32),
}
impl std::fmt::Display for WalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BadHeader => write!(f, "log segment header is damaged"),
Self::UnsupportedFormat(v) => write!(
f,
"log format version {v} is not the one this build reads ({WAL_FORMAT}); open \
the data directory with the Fusee version that wrote it"
),
}
}
}
impl std::error::Error for WalError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WalHeader {
pub group: u32,
pub segment: u64,
pub salt: u64,
pub base_seq: u64,
pub base_prev: u64,
pub created_ms: u64,
}
impl WalHeader {
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(WAL_HEADER_BYTES as usize);
out.extend_from_slice(WAL_MAGIC);
out.extend_from_slice(&WAL_FORMAT.to_le_bytes());
out.extend_from_slice(&self.group.to_le_bytes());
out.extend_from_slice(&self.segment.to_le_bytes());
out.extend_from_slice(&self.salt.to_le_bytes());
out.extend_from_slice(&self.base_seq.to_le_bytes());
out.extend_from_slice(&self.base_prev.to_le_bytes());
out.extend_from_slice(&self.created_ms.to_le_bytes());
let crc = crc32c::crc32c(&out);
out.extend_from_slice(&crc.to_le_bytes());
out.resize(WAL_HEADER_BYTES as usize, 0);
out
}
pub fn decode(bytes: &[u8]) -> Result<Option<Self>, WalError> {
const LEN: usize = 8 + 4 + 4 + 8 * 5;
let Some(head) = bytes.get(..LEN + 4) else {
return if bytes.iter().all(|b| *b == 0) {
Ok(None)
} else {
Err(WalError::BadHeader)
};
};
if head.iter().all(|b| *b == 0) {
return Ok(None);
}
if &head[..8] != WAL_MAGIC {
return Err(WalError::BadHeader);
}
let crc = u32::from_le_bytes(head[LEN..LEN + 4].try_into().unwrap());
if crc32c::crc32c(&head[..LEN]) != crc {
return Err(WalError::BadHeader);
}
let format = u32::from_le_bytes(head[8..12].try_into().unwrap());
if format != WAL_FORMAT {
return Err(WalError::UnsupportedFormat(format));
}
let u64_at = |i: usize| u64::from_le_bytes(head[i..i + 8].try_into().unwrap());
Ok(Some(Self {
group: u32::from_le_bytes(head[12..16].try_into().unwrap()),
segment: u64_at(16),
salt: u64_at(24),
base_seq: u64_at(32),
base_prev: u64_at(40),
created_ms: u64_at(48),
}))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SectionRef<'a> {
pub shard: u32,
pub first_lsn: Lsn,
pub count: u32,
pub records: &'a [u8],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BatchHeader {
pub seq: u64,
pub prev: u64,
pub acked: u64,
pub life: u64,
pub flags: u32,
}
pub fn batch_checksum(covered: &[u8]) -> u64 {
xxh64(covered, 0x4655_5345_4557_414c)
}
pub fn encode_batch(
out: &mut Vec<u8>,
salt: u64,
header: &BatchHeader,
sections: &[SectionRef<'_>],
align: usize,
) -> u64 {
debug_assert!(align.is_power_of_two() && align >= 8);
let start = out.len();
out.extend_from_slice(&[0; 8]); let body_len = BATCH_HEADER
+ sections
.iter()
.map(|s| SECTION_HEADER + s.records.len())
.sum::<usize>();
out.extend_from_slice(&(body_len as u32).to_le_bytes());
out.extend_from_slice(&header.flags.to_le_bytes());
out.extend_from_slice(&header.seq.to_le_bytes());
out.extend_from_slice(&header.prev.to_le_bytes());
out.extend_from_slice(&header.acked.to_le_bytes());
out.extend_from_slice(&header.life.to_le_bytes());
out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
for s in sections {
out.extend_from_slice(&s.shard.to_le_bytes());
out.extend_from_slice(&s.count.to_le_bytes());
out.extend_from_slice(&s.first_lsn.to_le_bytes());
out.extend_from_slice(&(s.records.len() as u32).to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(s.records);
}
debug_assert_eq!(out.len() - start, body_len);
let len = body_len.div_ceil(align) * align;
out.resize(start + len, 0);
out[start..start + 4].copy_from_slice(&(len as u32).to_le_bytes());
let covered = &out[start + 8..start + body_len];
let crc = crc32c::crc32c_append(crc32c::crc32c(&salt.to_le_bytes()), covered);
let checksum = batch_checksum(covered);
out[start + 4..start + 8].copy_from_slice(&crc.to_le_bytes());
checksum
}
pub fn batch_body_len(sections: &[SectionRef<'_>]) -> usize {
BATCH_HEADER
+ sections
.iter()
.map(|s| SECTION_HEADER + s.records.len())
.sum::<usize>()
}
pub fn batch_span(offset: u64, body_len: usize, pipelined: bool) -> u64 {
let packed = (body_len as u64).div_ceil(8) * 8;
if !pipelined {
return packed;
}
let span = (offset + body_len as u64).div_ceil(BLOCK) * BLOCK - offset;
if span > MAX_BATCH as u64 {
packed
} else {
span
}
}
fn set_span(batch: &mut Vec<u8>, span: u64) {
debug_assert!(span as usize >= batch.len() && span.is_multiple_of(8));
batch.resize(span as usize, 0);
batch[0..4].copy_from_slice(&(span as u32).to_le_bytes());
}
#[derive(Clone, Copy, Debug)]
pub struct BatchView<'a> {
pub header: BatchHeader,
pub len: usize,
pub checksum: u64,
body: &'a [u8],
sections: u32,
}
impl<'a> BatchView<'a> {
pub fn sections(&self) -> impl Iterator<Item = SectionRef<'a>> + '_ {
let body = self.body;
let mut pos = 0;
(0..self.sections).map(move |_| {
let h = &body[pos..pos + SECTION_HEADER];
let bytes = u32::from_le_bytes(h[16..20].try_into().unwrap()) as usize;
let records = &body[pos + SECTION_HEADER..pos + SECTION_HEADER + bytes];
pos += SECTION_HEADER + bytes;
SectionRef {
shard: u32::from_le_bytes(h[0..4].try_into().unwrap()),
count: u32::from_le_bytes(h[4..8].try_into().unwrap()),
first_lsn: u64::from_le_bytes(h[8..16].try_into().unwrap()),
records,
}
})
}
}
pub fn section_records(s: &SectionRef<'_>) -> DecodeResult<Vec<(Lsn, RecordBuf)>> {
let mut r = Reader::new(s.records);
let mut names = crate::record::InlineDecoder::default();
let mut out = Vec::with_capacity(s.count as usize);
while !r.is_empty() {
let body = r.bytes()?;
if is_offloaded(body) {
return Err(DecodeError(
"the section holds an offloaded record: read it with the log's blob area",
));
}
out.push((
s.first_lsn + out.len() as u64,
decode_body(body, &mut names)?,
));
}
if out.len() != s.count as usize {
return Err(DecodeError("section record count mismatch"));
}
Ok(out)
}
pub fn encode_records<S: AsRef<str>, B: AsRef<[u8]>>(
records: &[crate::record::Record<S, B>],
out: &mut Vec<u8>,
) {
for r in records {
encode_canonical(r, out);
}
}
enum Read<'a> {
Zero,
Valid(BatchView<'a>),
Bad,
More,
}
fn read_batch(bytes: &[u8], pos: usize, salt: u64, complete: bool) -> Read<'_> {
let Some(head) = bytes.get(pos..pos + BATCH_HEADER) else {
if !complete {
return Read::More;
}
return if bytes.get(pos..).is_none_or(|r| r.iter().all(|b| *b == 0)) {
Read::Zero
} else {
Read::Bad
};
};
if head.iter().all(|b| *b == 0) {
return Read::Zero;
}
let u32_at = |i: usize| u32::from_le_bytes(head[i..i + 4].try_into().unwrap());
let u64_at = |i: usize| u64::from_le_bytes(head[i..i + 8].try_into().unwrap());
let len = u32_at(0) as usize;
let body_len = u32_at(8) as usize;
if !len.is_multiple_of(8) || body_len < BATCH_HEADER || body_len > len || len > MAX_BATCH {
return Read::Bad;
}
let Some(covered) = bytes.get(pos + 8..pos + body_len) else {
return if complete { Read::Bad } else { Read::More };
};
let crc = crc32c::crc32c_append(crc32c::crc32c(&salt.to_le_bytes()), covered);
if crc != u32_at(4) {
return Read::Bad;
}
let sections = u32_at(48);
let body = &bytes[pos + BATCH_HEADER..pos + body_len];
let mut at = 0usize;
for _ in 0..sections {
let Some(h) = body.get(at..at + SECTION_HEADER) else {
return Read::Bad;
};
let n = u32::from_le_bytes(h[16..20].try_into().unwrap()) as usize;
at = match at.checked_add(SECTION_HEADER + n) {
Some(a) if a <= body.len() => a,
_ => return Read::Bad,
};
}
if at != body.len() {
return Read::Bad;
}
Read::Valid(BatchView {
header: BatchHeader {
seq: u64_at(16),
prev: u64_at(24),
acked: u64_at(32),
life: u64_at(40),
flags: u32_at(12),
},
len,
checksum: batch_checksum(covered),
body,
sections,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum End {
Clean,
Torn,
Damaged { at: usize, later_seq: u64 },
Full,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Scanned {
pub header: WalHeader,
pub end: usize,
pub last_seq: u64,
pub last_checksum: u64,
pub batches: u64,
pub tail: End,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Progress {
Done(Scanned),
NeedMore,
}
pub struct WalScanner {
header: WalHeader,
pos: usize,
seq: u64,
checksum: u64,
batches: u64,
}
impl WalScanner {
pub fn start(bytes: &[u8], complete: bool) -> Result<Option<Self>, WalError> {
debug_assert!(complete || bytes.len() >= WAL_HEADER_BYTES as usize);
let Some(header) = WalHeader::decode(bytes)? else {
return Ok(None);
};
Ok(Some(Self {
header,
pos: WAL_HEADER_BYTES as usize,
seq: header.base_seq.saturating_sub(1),
checksum: header.base_prev,
batches: 0,
}))
}
pub fn header(&self) -> &WalHeader {
&self.header
}
pub fn advance(
&mut self,
bytes: &[u8],
complete: bool,
segment_len: usize,
visit: &mut dyn FnMut(&BatchView<'_>),
) -> Progress {
let salt = self.header.salt;
let tail = loop {
if self.pos + BATCH_HEADER > segment_len {
break End::Full;
}
match read_batch(bytes, self.pos, salt, complete) {
Read::More => return Progress::NeedMore,
Read::Zero => break End::Clean,
Read::Valid(b)
if b.header.seq == self.seq + 1
&& b.header.prev == self.checksum
&& self.pos + b.len <= segment_len =>
{
visit(&b);
self.seq = b.header.seq;
self.checksum = b.checksum;
self.batches += 1;
self.pos += b.len;
}
Read::Valid(_) | Read::Bad => match self.find_proof(bytes, complete, segment_len) {
Some(t) => break t,
None => return Progress::NeedMore,
},
}
};
Progress::Done(Scanned {
header: self.header,
end: self.pos,
last_seq: self.seq,
last_checksum: self.checksum,
batches: self.batches,
tail,
})
}
fn find_proof(&self, bytes: &[u8], complete: bool, segment_len: usize) -> Option<End> {
let limit = (self.pos + DAMAGE_WINDOW).min(segment_len);
let mut stop = limit;
let mut block = self.pos.div_ceil(BLOCK as usize) * BLOCK as usize;
while block + BLOCK as usize <= limit.min(bytes.len()) {
if bytes[block..block + BLOCK as usize].iter().all(|b| *b == 0) {
stop = block;
break;
}
block += BLOCK as usize;
}
if stop == limit && !complete && bytes.len() < limit {
return None;
}
let region = &bytes[..stop.min(bytes.len())];
let mut at = self.pos + 8;
while at + BATCH_HEADER <= region.len() {
if let Read::Valid(b) = read_batch(region, at, self.header.salt, true)
&& b.header.seq > self.seq + 1
&& b.header.acked > self.seq
{
return Some(End::Damaged {
at: self.pos,
later_seq: b.header.seq,
});
}
at += 8;
}
Some(End::Torn)
}
}
#[derive(Clone, Debug)]
pub struct WalWriter {
group: u32,
life: u64,
segment: u64,
salt: u64,
segment_bytes: u64,
offset: u64,
next_seq: u64,
prev: u64,
started: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Placed {
pub segment: u64,
pub offset: u64,
pub bytes: Vec<u8>,
pub header: Option<WalHeader>,
pub seq: u64,
}
impl WalWriter {
#[allow(clippy::too_many_arguments)]
pub fn resume(
group: u32,
life: u64,
segment: u64,
salt: u64,
segment_bytes: u64,
offset: u64,
next_seq: u64,
prev: u64,
) -> Self {
Self {
group,
life,
segment,
salt,
segment_bytes,
offset: offset.div_ceil(8) * 8,
next_seq,
prev,
started: false,
}
}
pub fn next_seq(&self) -> u64 {
self.next_seq
}
pub fn segment(&self) -> u64 {
self.segment
}
pub fn offset(&self) -> u64 {
self.offset
}
pub fn roll(&mut self, segment: u64, salt: u64, segment_bytes: u64, now_ms: u64) -> WalHeader {
self.segment = segment;
self.salt = salt;
self.segment_bytes = segment_bytes;
self.offset = WAL_HEADER_BYTES;
WalHeader {
group: self.group,
segment,
salt,
base_seq: self.next_seq,
base_prev: self.prev,
created_ms: now_ms,
}
}
pub fn place(
&mut self,
sections: &[SectionRef<'_>],
acked: u64,
pipelined: bool,
now_ms: u64,
next_segment: impl FnOnce() -> (u64, u64, u64),
) -> Placed {
let header = BatchHeader {
seq: self.next_seq,
prev: self.prev,
acked,
life: self.life,
flags: if self.started { 0 } else { FLAG_LIFE_START },
};
let body = batch_body_len(sections);
debug_assert!(body <= MAX_BATCH, "recovery refuses a longer batch");
let mut bytes = Vec::with_capacity(body + BLOCK as usize);
let mut checksum = encode_batch(&mut bytes, self.salt, &header, sections, 8);
set_span(&mut bytes, batch_span(self.offset, body, pipelined));
let mut new_header = None;
if self.offset + bytes.len() as u64 > self.segment_bytes {
let (segment, salt, size) = next_segment();
self.segment = segment;
self.salt = salt;
self.segment_bytes = size;
self.offset = WAL_HEADER_BYTES;
new_header = Some(WalHeader {
group: self.group,
segment,
salt,
base_seq: self.next_seq,
base_prev: self.prev,
created_ms: now_ms,
});
bytes.clear();
checksum = encode_batch(&mut bytes, salt, &header, sections, 8);
set_span(&mut bytes, batch_span(self.offset, body, pipelined));
}
let placed = Placed {
segment: self.segment,
offset: self.offset,
bytes,
header: new_header,
seq: self.next_seq,
};
self.offset += placed.bytes.len() as u64;
self.prev = checksum;
self.next_seq += 1;
self.started = true;
placed
}
}
#[derive(Clone, Debug)]
pub struct SectionWriter {
next_lsn: Lsn,
first: Lsn,
count: u32,
buf: Vec<u8>,
start: usize,
last: usize,
}
const SECTION_BYTES: usize = 1024;
pub const MAX_SECTION: usize = 8 << 20;
impl SectionWriter {
pub fn new(next_lsn: Lsn) -> Self {
Self {
next_lsn,
first: next_lsn,
count: 0,
buf: Vec::new(),
start: 0,
last: 0,
}
}
pub fn next_lsn(&self) -> Lsn {
self.next_lsn
}
pub fn last_lsn(&self) -> Lsn {
self.next_lsn - 1
}
pub fn has_pending(&self) -> bool {
self.count > 0
}
pub fn append<S: AsRef<str>, B: AsRef<[u8]>>(
&mut self,
rec: &crate::record::Record<S, B>,
) -> Result<Lsn, crate::segment::TooLarge> {
if self.buf.capacity() == 0 {
self.buf = crate::segment::spare_bytes(SECTION_BYTES);
}
let at = self.buf.len();
encode_canonical(rec, &mut self.buf);
if self.buf.len() - at + BATCH_HEADER + SECTION_HEADER > MAX_BATCH {
self.buf.truncate(at);
return Err(crate::segment::TooLarge);
}
if self.count == 0 {
self.first = self.next_lsn;
}
self.last = at;
self.count += 1;
let lsn = self.next_lsn;
self.next_lsn += 1;
Ok(lsn)
}
pub fn last_encoded(&self) -> &[u8] {
self.buf.get(self.last..).unwrap_or_default()
}
pub fn discard(&mut self) -> Option<(u32, u64, Lsn)> {
if self.count == 0 {
return None;
}
let out = (
self.count,
(self.buf.len() - self.start) as u64,
self.next_lsn - 1,
);
self.buf.clear();
self.start = 0;
self.last = 0;
self.count = 0;
self.first = self.next_lsn;
Some(out)
}
pub fn take(&mut self) -> Option<crate::segment::Batch> {
self.take_up_to(usize::MAX)
}
pub fn take_up_to(&mut self, max: usize) -> Option<crate::segment::Batch> {
if self.count == 0 {
return None;
}
let pending = self.buf.len() - self.start;
let (section, records) = if pending <= max {
if self.start > 0 {
self.buf.drain(..self.start);
}
self.start = 0;
self.last = 0;
(std::mem::take(&mut self.buf), self.count)
} else {
let mut r = Reader::new(&self.buf[self.start..]);
let (mut end, mut records) = (0usize, 0u32);
while records < self.count && r.blob().is_ok() {
let at = pending - r.remaining();
if records > 0 && at > max {
break;
}
end = at;
records += 1;
}
let from = self.start;
let section = self.buf[from..from + end].to_vec();
self.start += end;
if self.start >= self.buf.len() / 2 {
self.buf.drain(..self.start);
self.last = match self.last.checked_sub(self.start) {
Some(last) => last,
None => self.buf.len(),
};
self.start = 0;
}
(section, records)
};
let first = self.first;
self.first += u64::from(records);
self.count -= records;
Some(crate::segment::Batch {
chunks: Vec::new(),
first_lsn: first,
last_lsn: self.first - 1,
records,
bytes: section.len() as u64,
section,
})
}
}
pub const OFFLOADED: u8 = 0x7f;
pub const BLOB_ALIGN: u64 = BLOCK;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OffloadRef {
pub kind: u8,
pub file: u64,
pub offset: u64,
pub len: u64,
pub crc: u32,
}
impl OffloadRef {
pub fn encode(&self, out: &mut Vec<u8>) {
let mut body = Vec::with_capacity(32);
body.push(OFFLOADED);
body.push(self.kind);
crate::wire::put_varint(&mut body, self.file);
crate::wire::put_varint(&mut body, self.offset);
crate::wire::put_varint(&mut body, self.len);
body.extend_from_slice(&self.crc.to_le_bytes());
crate::wire::put_bytes(out, &body);
}
pub fn decode(body: &[u8]) -> DecodeResult<Self> {
let mut r = Reader::new(body);
if r.u8()? != OFFLOADED {
return Err(DecodeError("not an offload reference"));
}
let kind = r.u8()?;
let file = r.varint()?;
let offset = r.varint()?;
let len = r.varint()?;
let crc = r.u32()?;
if !r.is_empty() {
return Err(DecodeError("trailing bytes after offload reference"));
}
if len as usize > crate::wire::MAX_PAYLOAD + 64 || !offset.is_multiple_of(BLOB_ALIGN) {
return Err(DecodeError("bad offload reference"));
}
Ok(Self {
kind,
file,
offset,
len,
crc,
})
}
pub fn matches(&self, body: &[u8]) -> bool {
body.len() as u64 == self.len
&& crc32c::crc32c(body) == self.crc
&& body.first() == Some(&self.kind)
}
}
#[inline]
pub fn is_offloaded(body: &[u8]) -> bool {
body.first() == Some(&OFFLOADED)
}
#[derive(Clone, Copy, Debug)]
pub struct SectionRecord<'a> {
pub at: usize,
pub body: &'a [u8],
}
pub fn section_spans(records: &[u8]) -> impl Iterator<Item = DecodeResult<SectionRecord<'_>>> {
let mut r = Reader::new(records);
let total = records.len();
std::iter::from_fn(move || {
if r.is_empty() {
return None;
}
let at = total - r.remaining();
Some(r.bytes().map(|body| SectionRecord { at, body }))
})
}
pub fn for_each_offload(records: &[u8], mut f: impl FnMut(u32, OffloadRef)) -> DecodeResult<()> {
for (i, rec) in section_spans(records).enumerate() {
let rec = rec?;
if is_offloaded(rec.body) {
f(i as u32, OffloadRef::decode(rec.body)?);
}
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Large {
pub index: u32,
pub at: usize,
pub body: std::ops::Range<usize>,
pub crc: u32,
}
pub fn find_large(records: &[u8], threshold: usize) -> DecodeResult<Vec<Large>> {
let mut out = Vec::new();
if records.len() <= threshold {
return Ok(out);
}
for (i, rec) in section_spans(records).enumerate() {
let rec = rec?;
if rec.body.len() > threshold && !is_offloaded(rec.body) {
let start = rec.body.as_ptr() as usize - records.as_ptr() as usize;
out.push(Large {
index: i as u32,
at: rec.at,
body: start..start + rec.body.len(),
crc: crc32c::crc32c(rec.body),
});
}
}
Ok(out)
}
#[derive(Debug)]
pub enum ResolveError {
Decode(DecodeError),
Blob {
at: Lsn,
r: OffloadRef,
why: String,
},
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Decode(e) => write!(f, "{e}"),
Self::Blob { at, r, why } => write!(
f,
"record {at} references bytes {}..{} of blob file {} ({why})",
r.offset,
r.offset + r.len,
r.file
),
}
}
}
impl From<DecodeError> for ResolveError {
fn from(e: DecodeError) -> Self {
Self::Decode(e)
}
}
pub fn section_records_resolved(
s: &SectionRef<'_>,
after: Lsn,
fetch: &mut dyn FnMut(&OffloadRef) -> std::io::Result<Vec<u8>>,
) -> Result<Vec<(Lsn, RecordBuf)>, ResolveError> {
let mut r = Reader::new(s.records);
let mut names = crate::record::InlineDecoder::default();
let mut out = Vec::with_capacity(s.count as usize);
let mut n = 0u64;
while !r.is_empty() {
let body = r.bytes()?;
let lsn = s.first_lsn + n;
n += 1;
if lsn <= after {
continue;
}
let rec = if is_offloaded(body) {
let reference = OffloadRef::decode(body)?;
let fail = |why: String| ResolveError::Blob {
at: lsn,
r: reference,
why,
};
let bytes = fetch(&reference).map_err(|e| fail(e.to_string()))?;
if !reference.matches(&bytes) {
return Err(fail(if bytes.len() as u64 != reference.len {
format!("{} bytes there", bytes.len())
} else {
"checksum mismatch".into()
}));
}
decode_body(&bytes, &mut names)?
} else {
decode_body(body, &mut names)?
};
out.push((lsn, rec));
}
if n != u64::from(s.count) {
return Err(DecodeError("section record count mismatch").into());
}
Ok(out)
}
#[cfg(test)]
mod tests;