use std::io::{Error, ErrorKind, Result};
use crate::Cno;
use crate::ondisk::BMapRawType;
pub const METADATA_KEY: &str = "hf-compaction";
const VERSION: &str = "v1";
#[derive(Clone, PartialEq, Eq)]
pub struct PendingCompaction {
pub base: Cno,
pub base_fingerprint: u64,
pub parts: (u16, u16),
pub moved: u64,
pub root: BMapRawType,
}
impl std::fmt::Debug for PendingCompaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PendingCompaction {{ base: {}, base_fingerprint: {:#018x}, parts: {}..{}, moved: {} }}",
self.base, self.base_fingerprint, self.parts.0, self.parts.1, self.moved)
}
}
impl PendingCompaction {
pub fn encode(&self) -> String {
let root: String = self.root.iter().map(|b| format!("{:02x}", b)).collect();
format!("{};base={};fp={:016x};parts={}-{};moved={};root={}",
VERSION, self.base, self.base_fingerprint, self.parts.0, self.parts.1, self.moved, root)
}
pub fn parse(value: &str) -> Result<Self> {
let bad = |why: &str| Error::new(ErrorKind::InvalidData,
format!("compaction metadata {:?}: {}", value, why));
let mut fields = value.split(';');
if fields.next() != Some(VERSION) {
return Err(bad("not a version this build reads"));
}
let mut get = |name: &str| -> Result<&str> {
let f = fields.next().ok_or_else(|| bad(&format!("no {}", name)))?;
f.strip_prefix(name).and_then(|f| f.strip_prefix('='))
.ok_or_else(|| bad(&format!("{} expected where {:?} is", name, f)))
};
let base: Cno = get("base")?.parse().map_err(|_| bad("base is not a number"))?;
let fp = get("fp")?;
let base_fingerprint = u64::from_str_radix(fp, 16).map_err(|_| bad("fp is not hexadecimal"))?;
let (p0, p1) = get("parts")?.split_once('-').ok_or_else(|| bad("parts is not p0-p1"))?;
let p0: u16 = p0.parse().map_err(|_| bad("p0 is not a part"))?;
let p1: u16 = p1.parse().map_err(|_| bad("p1 is not a part"))?;
let moved: u64 = get("moved")?.parse().map_err(|_| bad("moved is not a number"))?;
let hex = get("root")?;
let mut root = [0u8; std::mem::size_of::<BMapRawType>()];
if hex.len() != root.len() * 2 || !hex.is_ascii() {
return Err(bad("root is not one map root long"));
}
for (i, b) in root.iter_mut().enumerate() {
*b = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).map_err(|_| bad("root is not hexadecimal"))?;
}
if fields.next().is_some() {
return Err(bad("more fields than this version has"));
}
if base == 0 || p0 == 0 || p0 >= p1 || p1 as usize > crate::segment::Segment::MAX_PARTS {
return Err(bad("base, or the part range, is not one a compaction writes"));
}
Ok(Self { base, base_fingerprint, parts: (p0, p1), moved, root })
}
}
pub(crate) fn check_inode_write(buf: &[u8], compaction: Option<&PendingCompaction>) -> Result<()> {
if buf.len() != std::mem::size_of::<crate::ondisk::InodeRaw>() {
return Ok(());
}
let raw = crate::ondisk::InodeRaw::from_u8_slice(buf);
let bad = |why: String| Err(Error::new(ErrorKind::InvalidInput, why));
match compaction {
None if raw.is_compaction_pending() =>
bad("an inode write sets the attached-compaction bit without the compaction's metadata".into()),
Some(_) if !raw.is_compaction_pending() =>
bad("an inode write carries a compaction's metadata without the attached-compaction bit".into()),
Some(c) if c.base > raw.i_last_cno =>
bad(format!("a compaction built on checkpoint {} attached to checkpoint {}", c.base, raw.i_last_cno)),
_ => Ok(()),
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum StoredCompaction {
#[default]
None,
Pending(PendingCompaction),
Unreadable(String),
}
impl StoredCompaction {
pub fn from_metadata(value: Option<&str>) -> Self {
match value {
None => Self::None,
Some(v) => match PendingCompaction::parse(v) {
Ok(p) => Self::Pending(p),
Err(e) => Self::Unreadable(e.to_string()),
},
}
}
pub fn pending(&self) -> Option<&PendingCompaction> {
match self {
Self::Pending(p) => Some(p),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> PendingCompaction {
let mut root = [0u8; 56];
for (i, b) in root.iter_mut().enumerate() { *b = (i * 37) as u8; }
PendingCompaction { base: 10, base_fingerprint: 0xdead_beef_0123_4567, parts: (3, 9), moved: 4096, root }
}
#[test]
fn round_trip() {
let p = sample();
let v = p.encode();
assert!(v.is_ascii() && v.len() < 2048, "{}", v);
assert!(v.starts_with("v1;base=10;fp=deadbeef01234567;parts=3-9;moved=4096;root="), "{}", v);
assert_eq!(PendingCompaction::parse(&v).unwrap(), p);
assert_eq!(StoredCompaction::from_metadata(Some(&v)), StoredCompaction::Pending(p));
assert_eq!(StoredCompaction::from_metadata(None), StoredCompaction::None);
}
#[test]
fn an_inode_write_agrees_with_its_metadata() {
let mut raw = crate::ondisk::InodeRaw::default();
raw.i_last_cno = 12;
let p = sample();
assert!(check_inode_write(raw.as_u8_slice(), None).is_ok());
assert_eq!(check_inode_write(raw.as_u8_slice(), Some(&p)).unwrap_err().kind(), ErrorKind::InvalidInput);
raw.set_compaction_pending(true);
assert_eq!(check_inode_write(raw.as_u8_slice(), None).unwrap_err().kind(), ErrorKind::InvalidInput);
assert!(check_inode_write(raw.as_u8_slice(), Some(&p)).is_ok());
raw.i_last_cno = 9;
assert_eq!(check_inode_write(raw.as_u8_slice(), Some(&p)).unwrap_err().kind(), ErrorKind::InvalidInput);
}
#[test]
fn what_is_refused() {
let v = sample().encode();
let root = v.split("root=").nth(1).unwrap().to_string();
let bad = [
v.replacen("v1", "v2", 1),
v.replacen("base=10", "base=x", 1),
v.replacen("base=10", "base=0", 1),
v.replacen(";fp=", ";fq=", 1),
v.replacen("parts=3-9", "parts=9-3", 1),
v.replacen("parts=3-9", "parts=0-9", 1),
v.replacen("parts=3-9", "parts=3-20000", 1),
v.replacen("parts=3-9", "parts=3", 1),
v.replacen(";moved=4096", "", 1),
v.replacen(&root, &root[2..], 1),
v.replacen(&root, &format!("zz{}", &root[2..]), 1),
format!("{};extra=1", v),
String::new(),
];
for b in bad {
let e = PendingCompaction::parse(&b).expect_err(&b);
assert_eq!(e.kind(), ErrorKind::InvalidData, "{}", b);
assert!(matches!(StoredCompaction::from_metadata(Some(&b)), StoredCompaction::Unreadable(_)), "{}", b);
}
}
}