use crate::block::BlockDevice;
use crate::{Error, Result};
use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use super::tag::{self, Tag};
#[derive(Debug, Clone, Copy)]
pub struct Geom {
pub block_size: u32,
pub block_count: u32,
pub prog_size: u32,
pub fcrc: bool,
}
impl Geom {
pub fn offset(&self, block: u32) -> u64 {
block as u64 * self.block_size as u64
}
pub fn commit_limit(&self) -> usize {
self.block_size as usize - 48
}
pub fn split_limit(&self) -> usize {
let half = (self.block_size as usize / 2).next_multiple_of(self.prog_size.max(1) as usize);
half.min(self.commit_limit())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Struct {
Dir([u32; 2]),
Inline(Vec<u8>),
Ctz { head: u32, size: u32 },
}
#[derive(Debug, Clone, Default)]
pub struct Entry {
pub kind: u8,
pub name: Vec<u8>,
pub data: Option<Struct>,
pub attrs: Vec<(u8, Vec<u8>)>,
}
impl Entry {
pub fn is_file(&self) -> bool {
self.kind == tag::TYPE_REG as u8 || self.kind == tag::TYPE_DIR as u8
}
fn commit_size(&self) -> usize {
let mut n = 4 + self.name.len();
n += match &self.data {
Some(Struct::Dir(_)) | Some(Struct::Ctz { .. }) => 4 + 8,
Some(Struct::Inline(d)) => 4 + d.len(),
None => 0,
};
for (_, v) in &self.attrs {
n += 4 + v.len();
}
n
}
}
#[derive(Debug, Clone)]
pub struct Mdir {
pub pair: [u32; 2],
pub rev: u32,
pub entries: Vec<Entry>,
pub tail: Option<[u32; 2]>,
pub hard: bool,
pub gdelta: Option<[u8; 12]>,
pub fcrc_size: Option<u32>,
}
impl Mdir {
pub fn empty(pair: [u32; 2]) -> Self {
Self {
pair,
rev: 0,
entries: Vec::new(),
tail: None,
hard: false,
gdelta: None,
fcrc_size: None,
}
}
pub fn find(&self, name: &[u8]) -> Option<usize> {
self.entries
.iter()
.position(|e| e.is_file() && e.name == name)
}
fn entries_size(&self) -> usize {
self.entries.iter().map(Entry::commit_size).sum()
}
}
pub fn fetch(dev: &mut dyn BlockDevice, geom: &Geom, pair: [u32; 2]) -> Result<Mdir> {
for b in pair {
if b >= geom.block_count {
return Err(Error::InvalidImage(format!(
"littlefs: metadata pair block {b} beyond block count {}",
geom.block_count
)));
}
}
let mut order = pair;
if let (Some(a), Some(b)) = (read_rev(dev, geom, pair[0]), read_rev(dev, geom, pair[1]))
&& tag::rev_newer(b, a)
{
order.swap(0, 1);
}
for i in 0..2 {
let block = order[i];
if let Some(mut mdir) = parse_block(dev, geom, block)? {
mdir.pair = [block, order[1 - i]];
return Ok(mdir);
}
}
Err(Error::InvalidImage(format!(
"littlefs: corrupted metadata pair {{{}, {}}}",
pair[0], pair[1]
)))
}
pub fn read_rev(dev: &mut dyn BlockDevice, geom: &Geom, block: u32) -> Option<u32> {
let mut b = [0u8; 4];
dev.read_at(geom.offset(block), &mut b).ok()?;
Some(u32::from_le_bytes(b))
}
fn parse_block(dev: &mut dyn BlockDevice, geom: &Geom, block: u32) -> Result<Option<Mdir>> {
let bs = geom.block_size as usize;
let mut buf = vec![0u8; bs];
if dev.read_at(geom.offset(block), &mut buf).is_err() {
return Ok(None);
}
let rev = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let mut live: Option<Mdir> = None;
let mut cur = Mdir::empty([block, block]);
cur.rev = rev;
let mut off = 0usize;
let mut ptag = tag::PTAG_INIT;
let mut crc = tag::crc(tag::PTAG_INIT, &buf[0..4]);
loop {
off += Tag(ptag).dsize();
if off + 4 > bs {
break;
}
crc = tag::crc(crc, &buf[off..off + 4]);
let t = Tag(tag::be32(&buf[off..off + 4]) ^ ptag);
if !t.is_valid() || off + t.dsize() > bs {
break;
}
ptag = t.0;
if t.type2() == tag::TYPE_CCRC {
if off + 8 > bs {
break;
}
if crc != tag::le32(&buf[off + 4..off + 8]) {
break;
}
ptag ^= ((t.chunk() & 1) as u32) << 31;
live = Some(cur.clone());
crc = tag::PTAG_INIT;
continue;
}
let data = &buf[off + 4..off + t.dsize()];
crc = tag::crc(crc, data);
apply(&mut cur, t, data);
}
Ok(live)
}
fn apply(mdir: &mut Mdir, t: Tag, data: &[u8]) {
let id = t.id() as usize;
match t.type1() {
tag::T1_NAME => {
grow(mdir, id);
if let Some(e) = mdir.entries.get_mut(id) {
e.kind = t.chunk();
e.name = data.to_vec();
}
}
tag::T1_STRUCT => {
grow(mdir, id);
let Some(e) = mdir.entries.get_mut(id) else {
return;
};
e.data = match t.type3() {
tag::TYPE_DIRSTRUCT if data.len() >= 8 => Some(Struct::Dir([
tag::le32(&data[0..4]),
tag::le32(&data[4..8]),
])),
tag::TYPE_CTZSTRUCT if data.len() >= 8 => Some(Struct::Ctz {
head: tag::le32(&data[0..4]),
size: tag::le32(&data[4..8]),
}),
tag::TYPE_INLINESTRUCT => Some(Struct::Inline(data.to_vec())),
_ => e.data.take(),
};
}
tag::T1_USERATTR => {
grow(mdir, id);
let Some(e) = mdir.entries.get_mut(id) else {
return;
};
let key = t.chunk();
e.attrs.retain(|(k, _)| *k != key);
if !t.is_delete() {
e.attrs.push((key, data.to_vec()));
e.attrs.sort_by_key(|(k, _)| *k);
}
}
tag::T1_SPLICE => {
match t.type3() {
tag::TYPE_CREATE => {
if id <= mdir.entries.len() {
mdir.entries.insert(id, Entry::default());
} else {
grow(mdir, id);
}
}
tag::TYPE_DELETE if id < mdir.entries.len() => {
mdir.entries.remove(id);
}
_ => {}
}
}
tag::T1_TAIL => {
if data.len() >= 8 {
mdir.tail = Some([tag::le32(&data[0..4]), tag::le32(&data[4..8])]);
mdir.hard = t.chunk() & 1 != 0;
}
}
tag::T1_GSTATE => {
if data.len() >= 12 {
let mut g = [0u8; 12];
g.copy_from_slice(&data[..12]);
mdir.gdelta = Some(g);
}
}
tag::T1_CRC if t.type3() == tag::TYPE_FCRC && data.len() >= 8 => {
mdir.fcrc_size = Some(tag::le32(&data[0..4]));
}
_ => {}
}
}
fn grow(mdir: &mut Mdir, id: usize) {
if id >= tag::ID_NONE as usize {
return;
}
while mdir.entries.len() <= id {
mdir.entries.push(Entry::default());
}
}
struct CommitBuf {
buf: Vec<u8>,
ptag: u32,
}
impl CommitBuf {
fn new(rev: u32) -> Self {
Self {
buf: rev.to_le_bytes().to_vec(),
ptag: tag::PTAG_INIT,
}
}
fn push(&mut self, t: Tag, data: &[u8]) {
let stored = (t.0 & 0x7fff_ffff) ^ self.ptag;
self.buf.extend_from_slice(&stored.to_be_bytes());
if !t.is_delete() {
self.buf.extend_from_slice(data);
}
self.ptag = t.0 & 0x7fff_ffff;
}
fn finish(mut self, geom: &Geom) -> Result<Vec<u8>> {
let bs = geom.block_size as usize;
let prog = geom.prog_size.max(1) as usize;
let reserve = if geom.fcrc { 5 * 4 } else { 2 * 4 };
let end = (self.buf.len() + reserve).min(bs).next_multiple_of(prog);
if end > bs {
return Err(Error::InvalidArgument(
"littlefs: commit does not fit in a metadata block".into(),
));
}
let mut block = vec![0xffu8; bs];
if geom.fcrc && end <= bs - prog {
let fcrc_crc = tag::crc(tag::PTAG_INIT, &block[end..end + prog]);
let mut d = [0u8; 8];
d[0..4].copy_from_slice(&(prog as u32).to_le_bytes());
d[4..8].copy_from_slice(&fcrc_crc.to_le_bytes());
self.push(Tag::new(tag::TYPE_FCRC, tag::ID_NONE, 8), &d);
}
let pad = end - (self.buf.len() + 4);
if pad > tag::MAX_SIZE {
return Err(Error::InvalidArgument(
"littlefs: commit padding exceeds a single CRC tag".into(),
));
}
let eperturb: u8 = if end < bs { block[end] } else { 0xff };
let ccrc = Tag::new(
tag::TYPE_CCRC + ((!eperturb) >> 7) as u16,
tag::ID_NONE,
pad as u16,
);
let stored = (ccrc.0 & 0x7fff_ffff) ^ self.ptag;
self.buf.extend_from_slice(&stored.to_be_bytes());
let crc = tag::crc(tag::PTAG_INIT, &self.buf);
self.buf.extend_from_slice(&crc.to_le_bytes());
block[..self.buf.len()].copy_from_slice(&self.buf);
Ok(block)
}
}
pub fn write_compaction(
dev: &mut dyn BlockDevice,
geom: &Geom,
mdir: &Mdir,
block: u32,
rev: u32,
) -> Result<()> {
let mut c = CommitBuf::new(rev);
for (id, e) in mdir.entries.iter().enumerate() {
let id = id as u16;
c.push(
Tag::new(tag::TYPE_NAME | e.kind as u16, id, e.name.len() as u16),
&e.name,
);
match &e.data {
Some(Struct::Dir(p)) => {
let mut d = [0u8; 8];
d[0..4].copy_from_slice(&p[0].to_le_bytes());
d[4..8].copy_from_slice(&p[1].to_le_bytes());
c.push(Tag::new(tag::TYPE_DIRSTRUCT, id, 8), &d);
}
Some(Struct::Ctz { head, size }) => {
let mut d = [0u8; 8];
d[0..4].copy_from_slice(&head.to_le_bytes());
d[4..8].copy_from_slice(&size.to_le_bytes());
c.push(Tag::new(tag::TYPE_CTZSTRUCT, id, 8), &d);
}
Some(Struct::Inline(data)) => {
c.push(
Tag::new(tag::TYPE_INLINESTRUCT, id, data.len() as u16),
data,
);
}
None => {}
}
for (k, v) in &e.attrs {
c.push(
Tag::new(tag::TYPE_USERATTR | *k as u16, id, v.len() as u16),
v,
);
}
}
if let Some(t) = mdir.tail {
let mut d = [0u8; 8];
d[0..4].copy_from_slice(&t[0].to_le_bytes());
d[4..8].copy_from_slice(&t[1].to_le_bytes());
let ty = if mdir.hard {
tag::TYPE_HARDTAIL
} else {
tag::TYPE_SOFTTAIL
};
c.push(Tag::new(ty, tag::ID_NONE, 8), &d);
}
if let Some(g) = mdir.gdelta {
c.push(Tag::new(tag::TYPE_MOVESTATE, tag::ID_NONE, 12), &g);
}
let image = c.finish(geom)?;
dev.write_at(geom.offset(block), &image)
}
pub fn needs_split(geom: &Geom, mdir: &Mdir) -> bool {
mdir.entries_size() > geom.split_limit() || mdir.entries.len() >= 0xff
}
pub fn split_point(geom: &Geom, mdir: &Mdir) -> usize {
let end = mdir.entries.len();
let mut split = 0usize;
while end - split > 1 {
let size: usize = mdir.entries[split..end]
.iter()
.map(Entry::commit_size)
.sum();
if end - split < 0xff && size <= geom.split_limit() {
break;
}
split += (end - split) / 2;
}
split
}