mod dir;
mod file;
mod mdir;
#[cfg(test)]
mod tests;
pub use dir::{Dir, DirEntry, DirIter, Metadata};
pub use file::File;
use super::index;
use super::tag;
use super::{DISK_VERSION_2_0, DISK_VERSION_2_1, FILE_MAX, MAGIC, SUPERBLOCK_PAIR};
use mdir::{Commit, Data, Mdir, Struct, StructOut};
pub const LOOKAHEAD_BLOCKS: u32 = 256;
#[cfg(not(feature = "alloc"))]
const LOOKAHEAD_WORDS: usize = (LOOKAHEAD_BLOCKS as usize).div_ceil(32);
pub const MIN_BLOCK_SIZE: u32 = 128;
use crate::device::FlashDriver;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error<E> {
Io(E),
NotLittleFs,
UnsupportedVersion {
major: u16,
minor: u16,
},
GeometryMismatch {
volume: u32,
driver: u32,
},
BadGeometry,
ScratchTooSmall {
needed: usize,
got: usize,
},
Corrupt(&'static str),
NotFound,
NotADirectory,
IsADirectory,
AlreadyExists,
DirectoryNotEmpty,
InvalidName,
InvalidPath,
NoSpace,
CommitTooLarge,
FileTooLarge,
InvalidOffset,
AttrTooLarge,
Unsupported(&'static str),
}
impl<E> Error<E> {
pub fn is_not_found(&self) -> bool {
matches!(self, Error::NotFound)
}
}
impl<E: core::fmt::Display> core::fmt::Display for Error<E> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::Io(e) => write!(f, "device error: {e}"),
Error::NotLittleFs => f.write_str("not a littlefs volume"),
Error::UnsupportedVersion { major, minor } => {
write!(f, "unsupported littlefs disk version {major}.{minor}")
}
Error::GeometryMismatch { volume, driver } => {
write!(
f,
"volume is formatted for {volume}, driver reports {driver}"
)
}
Error::BadGeometry => f.write_str("geometry littlefs cannot use"),
Error::ScratchTooSmall { needed, got } => {
write!(f, "scratch buffer is {got} bytes, need {needed}")
}
Error::Corrupt(what) => write!(f, "corrupt volume: {what}"),
Error::NotFound => f.write_str("no such file or directory"),
Error::NotADirectory => f.write_str("not a directory"),
Error::IsADirectory => f.write_str("is a directory"),
Error::AlreadyExists => f.write_str("already exists"),
Error::DirectoryNotEmpty => f.write_str("directory not empty"),
Error::InvalidName => f.write_str("invalid name"),
Error::InvalidPath => f.write_str("invalid path"),
Error::NoSpace => f.write_str("no space left on volume"),
Error::CommitTooLarge => f.write_str("entry too large for a metadata block"),
Error::FileTooLarge => f.write_str("file would exceed the volume's limit"),
Error::InvalidOffset => f.write_str("offset out of range"),
Error::AttrTooLarge => f.write_str("attribute value too large"),
Error::Unsupported(what) => write!(f, "unsupported: {what}"),
}
}
}
#[cfg(feature = "std")]
impl<E: core::fmt::Debug + core::fmt::Display> std::error::Error for Error<E> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormatOpts {
pub block_count: Option<u32>,
pub disk_version: u32,
pub name_max: u32,
pub inline_max: Option<u32>,
}
impl Default for FormatOpts {
fn default() -> Self {
Self {
block_count: None,
disk_version: DISK_VERSION_2_1,
name_max: 255,
inline_max: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Geometry {
pub block_size: u32,
pub block_count: u32,
pub prog_size: u32,
pub version: u32,
pub name_max: u32,
pub file_max: u32,
pub attr_max: u32,
pub inline_max: u32,
}
impl Geometry {
fn fcrc(&self) -> bool {
self.version >= DISK_VERSION_2_1
}
pub fn version_parts(&self) -> (u16, u16) {
((self.version >> 16) as u16, (self.version & 0xffff) as u16)
}
fn commit_limit(&self) -> usize {
self.block_size as usize - 48
}
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())
}
fn default_inline_max(&self) -> u32 {
let ceiling = (tag::MAX_SIZE as u32).min(self.split_limit() as u32 / 2);
(self.block_size / 8).min(ceiling)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Placed {
pair: [u32; 2],
id: u16,
}
#[derive(Debug, Clone, Copy)]
enum Edit<'a> {
Nothing,
Insert {
at: u16,
kind: u8,
name: &'a [u8],
data: StructOut<'a>,
},
SetStruct { id: u16, data: StructOut<'a> },
Delete { id: u16 },
SetAttr {
id: u16,
key: u8,
value: Option<&'a [u8]>,
},
}
impl<'a> Edit<'a> {
fn count_delta(&self) -> i32 {
match self {
Edit::Insert { .. } => 1,
Edit::Delete { .. } => -1,
_ => 0,
}
}
fn focus(&self) -> Option<u16> {
match self {
Edit::Insert { at, .. } => Some(*at),
Edit::SetStruct { id, .. } | Edit::SetAttr { id, .. } => Some(*id),
Edit::Nothing | Edit::Delete { .. } => None,
}
}
fn item(&self, i: u16) -> Item<'a> {
match self {
Edit::Nothing => Item::Copy(i),
Edit::Insert {
at,
kind,
name,
data,
} => {
if i < *at {
Item::Copy(i)
} else if i == *at {
Item::New {
kind: *kind,
name,
data: *data,
}
} else {
Item::Copy(i - 1)
}
}
Edit::SetStruct { id, data } => {
if i == *id {
Item::CopyWithData(i, *data)
} else {
Item::Copy(i)
}
}
Edit::Delete { id } => {
if i < *id {
Item::Copy(i)
} else {
Item::Copy(i + 1)
}
}
Edit::SetAttr { id, key, value } => {
if i == *id {
Item::CopyWithAttr(i, *key, *value)
} else {
Item::Copy(i)
}
}
}
}
}
#[derive(Debug, Clone, Copy)]
enum Item<'a> {
Copy(u16),
CopyWithData(u16, StructOut<'a>),
CopyWithAttr(u16, u8, Option<&'a [u8]>),
New {
kind: u8,
name: &'a [u8],
data: StructOut<'a>,
},
}
#[derive(Debug)]
pub struct Volume<D: FlashDriver, const BLOCK: usize = 4096, const PROG: usize = 256> {
dev: D,
geom: Geometry,
buf: [u8; BLOCK],
stage: [u8; PROG],
cached: Option<Mdir>,
root: [u32; 2],
#[cfg(not(feature = "alloc"))]
look_start: u32,
#[cfg(not(feature = "alloc"))]
look: [u32; LOOKAHEAD_WORDS],
#[cfg(not(feature = "alloc"))]
look_next: u32,
#[cfg(not(feature = "alloc"))]
look_valid: bool,
pending_pair: Option<[u32; 2]>,
pending_ctz: Option<(u32, u32)>,
#[cfg(feature = "alloc")]
cursor: u32,
#[cfg(feature = "alloc")]
used: Option<::alloc::vec::Vec<u32>>,
}
impl<D: FlashDriver, const BLOCK: usize, const PROG: usize> Volume<D, BLOCK, PROG> {
pub fn mount(dev: D) -> Result<Self, Error<D::Error>> {
Self::check_scratch(&dev)?;
let provisional = Geometry {
block_size: dev.block_size(),
block_count: dev.block_count(),
prog_size: dev.prog_size().max(1),
version: DISK_VERSION_2_1,
name_max: tag::MAX_SIZE as u32,
file_max: FILE_MAX,
attr_max: tag::MAX_SIZE as u32,
inline_max: 0,
};
Self::check_geometry(&provisional)?;
let mut vol = Self::new(dev, provisional);
let m = match vol.fetch(SUPERBLOCK_PAIR) {
Ok(m) => m,
Err(Error::Corrupt(_)) => return Err(Error::NotLittleFs),
Err(e) => return Err(e),
};
let bs = vol.bs();
let Some((kind, name_off, name_len)) = mdir::name_of(&vol.buf[..bs], &m, 0) else {
return Err(Error::NotLittleFs);
};
if kind != tag::TYPE_SUPERBLOCK as u8
|| name_len as usize != MAGIC.len()
|| &vol.buf[name_off as usize..name_off as usize + MAGIC.len()] != MAGIC
{
return Err(Error::NotLittleFs);
}
let Some(Struct::Inline { off, len }) = mdir::struct_of(&vol.buf[..bs], &m, 0) else {
return Err(Error::NotLittleFs);
};
if len < 24 {
return Err(Error::NotLittleFs);
}
let cfg = &vol.buf[off as usize..off as usize + 24];
let version = tag::le32(&cfg[0..4]);
let block_size = tag::le32(&cfg[4..8]);
let block_count = tag::le32(&cfg[8..12]);
let mut geom = Geometry {
version,
name_max: tag::le32(&cfg[12..16]),
file_max: tag::le32(&cfg[16..20]),
attr_max: tag::le32(&cfg[20..24]),
..vol.geom
};
if version >> 16 != 2 || version & 0xffff > (DISK_VERSION_2_1 & 0xffff) {
return Err(Error::UnsupportedVersion {
major: (version >> 16) as u16,
minor: (version & 0xffff) as u16,
});
}
if block_size != vol.dev.block_size() {
return Err(Error::GeometryMismatch {
volume: block_size,
driver: vol.dev.block_size(),
});
}
if block_count == 0 || block_count > vol.dev.block_count() {
return Err(Error::GeometryMismatch {
volume: block_count,
driver: vol.dev.block_count(),
});
}
geom.block_count = block_count;
if geom.name_max == 0 || geom.name_max > tag::MAX_SIZE as u32 {
geom.name_max = 255;
}
if geom.file_max == 0 || geom.file_max > FILE_MAX {
geom.file_max = FILE_MAX;
}
if geom.attr_max == 0 || geom.attr_max > tag::MAX_SIZE as u32 {
geom.attr_max = tag::MAX_SIZE as u32;
}
geom.inline_max = geom.default_inline_max();
Self::check_geometry(&geom)?;
vol.geom = geom;
vol.root = SUPERBLOCK_PAIR;
let mut pair = Some(SUPERBLOCK_PAIR);
let mut hops = 0u32;
while let Some(p) = pair {
let m = vol.fetch(p)?;
if vol.is_superblock_pair(&m) {
vol.root = m.pair;
}
pair = m.tail;
hops += 1;
if hops > block_count {
return Err(Error::Corrupt("cycle in the metadata-pair list"));
}
}
Ok(vol)
}
pub fn format(dev: D) -> Result<Self, Error<D::Error>> {
Self::format_with(dev, &FormatOpts::default())
}
pub fn format_with(dev: D, opts: &FormatOpts) -> Result<Self, Error<D::Error>> {
Self::check_scratch(&dev)?;
if opts.disk_version != DISK_VERSION_2_0 && opts.disk_version != DISK_VERSION_2_1 {
return Err(Error::UnsupportedVersion {
major: (opts.disk_version >> 16) as u16,
minor: (opts.disk_version & 0xffff) as u16,
});
}
let avail = dev.block_count();
let block_count = opts.block_count.unwrap_or(avail);
if block_count > avail {
return Err(Error::GeometryMismatch {
volume: block_count,
driver: avail,
});
}
if opts.name_max == 0 || opts.name_max > tag::MAX_SIZE as u32 {
return Err(Error::BadGeometry);
}
let mut geom = Geometry {
block_size: dev.block_size(),
block_count,
prog_size: dev.prog_size().max(1),
version: opts.disk_version,
name_max: opts.name_max,
file_max: FILE_MAX,
attr_max: tag::MAX_SIZE as u32,
inline_max: 0,
};
geom.inline_max = match opts.inline_max {
Some(v) => {
let ceiling = (tag::MAX_SIZE as u32).min(geom.split_limit() as u32 / 2);
if v > ceiling {
return Err(Error::BadGeometry);
}
v
}
None => geom.default_inline_max(),
};
Self::check_geometry(&geom)?;
let mut vol = Self::new(dev, geom);
vol.root = SUPERBLOCK_PAIR;
let sb = vol.superblock_bytes();
for (i, block) in [SUPERBLOCK_PAIR[0], SUPERBLOCK_PAIR[1]]
.into_iter()
.enumerate()
{
let empty = Mdir::empty([block, block]);
vol.write_range(
&empty,
Edit::Insert {
at: 0,
kind: tag::TYPE_SUPERBLOCK as u8,
name: MAGIC,
data: StructOut::Inline(Data::Bytes(&sb)),
},
0,
1,
block,
i as u32 + 1,
None,
false,
None,
)?;
}
vol.cached = None;
vol.mark_used(SUPERBLOCK_PAIR[0]);
vol.mark_used(SUPERBLOCK_PAIR[1]);
Ok(vol)
}
fn new(dev: D, geom: Geometry) -> Self {
Self {
dev,
geom,
buf: [0u8; BLOCK],
stage: [0u8; PROG],
cached: None,
root: SUPERBLOCK_PAIR,
#[cfg(not(feature = "alloc"))]
look_start: 0,
#[cfg(not(feature = "alloc"))]
look: [0u32; LOOKAHEAD_WORDS],
#[cfg(not(feature = "alloc"))]
look_next: 0,
#[cfg(not(feature = "alloc"))]
look_valid: false,
pending_pair: None,
pending_ctz: None,
#[cfg(feature = "alloc")]
cursor: 0,
#[cfg(feature = "alloc")]
used: None,
}
}
fn check_scratch(dev: &D) -> Result<(), Error<D::Error>> {
let bs = dev.block_size() as usize;
if BLOCK < bs {
return Err(Error::ScratchTooSmall {
needed: bs,
got: BLOCK,
});
}
let prog = dev.prog_size().max(1) as usize;
if PROG < prog {
return Err(Error::ScratchTooSmall {
needed: prog,
got: PROG,
});
}
Ok(())
}
fn check_geometry(geom: &Geometry) -> Result<(), Error<D::Error>> {
if geom.block_size < MIN_BLOCK_SIZE
|| !geom.block_size.is_power_of_two()
|| !geom.prog_size.is_power_of_two()
|| geom.prog_size > geom.block_size
|| geom.block_count < 4
{
return Err(Error::BadGeometry);
}
Ok(())
}
fn superblock_bytes(&self) -> [u8; 24] {
let mut b = [0u8; 24];
for (i, v) in [
self.geom.version,
self.geom.block_size,
self.geom.block_count,
self.geom.name_max,
self.geom.file_max,
self.geom.attr_max,
]
.iter()
.enumerate()
{
b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
b
}
pub fn geometry(&self) -> &Geometry {
&self.geom
}
pub fn driver(&self) -> &D {
&self.dev
}
pub fn driver_mut(&mut self) -> &mut D {
&mut self.dev
}
pub fn sync(&mut self) -> Result<(), Error<D::Error>> {
self.dev.sync().map_err(Error::Io)
}
pub fn unmount(mut self) -> Result<D, Error<D::Error>> {
self.sync()?;
let Self { dev, .. } = self;
Ok(dev)
}
pub fn alloc_cache_bytes(&self) -> usize {
#[cfg(feature = "alloc")]
{
self.used.as_ref().map_or(0, |v| v.len() * 4)
}
#[cfg(not(feature = "alloc"))]
{
0
}
}
}
impl<D: FlashDriver, const BLOCK: usize, const PROG: usize> Volume<D, BLOCK, PROG> {
fn bs(&self) -> usize {
self.geom.block_size as usize
}
fn read_block(&mut self, block: u32) -> Result<(), Error<D::Error>> {
if block >= self.geom.block_count {
return Err(Error::Corrupt("block beyond the end of the volume"));
}
let bs = self.bs();
self.cached = None;
self.dev
.read(block, 0, &mut self.buf[..bs])
.map_err(Error::Io)
}
fn read_rev(&mut self, block: u32) -> Result<u32, Error<D::Error>> {
let mut b = [0u8; 4];
self.dev.read(block, 0, &mut b).map_err(Error::Io)?;
Ok(u32::from_le_bytes(b))
}
fn fetch(&mut self, pair: [u32; 2]) -> Result<Mdir, Error<D::Error>> {
for b in pair {
if b >= self.geom.block_count {
return Err(Error::Corrupt("metadata pair beyond the end of the volume"));
}
}
if let Some(m) = self.cached
&& ((m.pair[0] == pair[0] && m.pair[1] == pair[1])
|| (m.pair[0] == pair[1] && m.pair[1] == pair[0]))
{
return Ok(m);
}
let mut order = pair;
let a = self.read_rev(pair[0])?;
let b = self.read_rev(pair[1])?;
if tag::rev_newer(b, a) {
order.swap(0, 1);
}
for i in 0..2 {
let block = order[i];
self.read_block(block)?;
let bs = self.bs();
if let Some(m) = mdir::parse(&self.buf[..bs], [block, order[1 - i]]) {
self.cached = Some(m);
return Ok(m);
}
}
Err(Error::Corrupt("metadata pair holds no valid commit"))
}
fn load(&mut self, m: &Mdir) -> Result<(), Error<D::Error>> {
if self.cached.is_some_and(|c| c.pair[0] == m.pair[0]) {
return Ok(());
}
self.read_block(m.pair[0])?;
let bs = self.bs();
self.cached = mdir::parse(&self.buf[..bs], m.pair);
Ok(())
}
fn is_superblock_pair(&self, m: &Mdir) -> bool {
let bs = self.bs();
matches!(
mdir::name_of(&self.buf[..bs], m, 0),
Some((kind, _, _)) if kind == tag::TYPE_SUPERBLOCK as u8
)
}
fn commit(&mut self, m: &Mdir, edit: Edit<'_>) -> Result<CommitOut, Error<D::Error>> {
let outer_pending = self.pending_pair;
let out = self.commit_inner(m, edit);
self.pending_pair = outer_pending;
out
}
fn commit_inner(&mut self, m: &Mdir, edit: Edit<'_>) -> Result<CommitOut, Error<D::Error>> {
self.load(m)?;
let total = (m.count as i32 + edit.count_delta()).max(0);
if total > MAX_IDS as i32 + 1 {
return Err(Error::Unsupported(
"more than 255 entries in a metadata pair",
));
}
let total = total as u16;
let mut sizes = [0u16; MAX_IDS + 1];
{
let bs = self.bs();
let src = &self.buf[..bs];
let keys = mdir::attr_keys(src, m);
for i in 0..total {
sizes[i as usize] = item_size(src, m, &keys, &edit.item(i)) as u16;
}
}
let span = |lo: u16, hi: u16| -> usize {
sizes[lo as usize..hi as usize]
.iter()
.map(|n| *n as usize)
.sum()
};
let mut focus = edit.focus().map(|id| Placed { pair: m.pair, id });
let mut placed = None;
let mut end = total;
let mut tail = m.tail;
let mut hard = m.hard;
let limit = self.geom.split_limit();
while span(0, end) > limit || end as usize >= MAX_IDS {
let at = split_point(&sizes, end, limit);
if at == 0 {
return Err(Error::CommitTooLarge);
}
let fresh = self.alloc_pair()?;
let rev = self.read_rev(fresh[0])?.wrapping_add(1);
self.pending_pair = Some(fresh);
self.write_range(m, edit, at, end, fresh[1], rev, tail, hard, None)?;
let fresh = [fresh[1], fresh[0]];
if let Some(f) = focus
&& f.id >= at
&& f.id < end
{
placed = Some(Placed {
pair: fresh,
id: f.id - at,
});
focus = None;
}
tail = Some(fresh);
hard = true;
end = at;
}
let rev = m.rev.wrapping_add(1);
self.write_range(m, edit, 0, end, m.target(), rev, tail, hard, m.gdelta)?;
let head = [m.pair[1], m.pair[0]];
if let Some(f) = focus
&& f.id < end
{
placed = Some(Placed {
pair: head,
id: f.id,
});
}
self.cached = None;
Ok(CommitOut { pair: head, placed })
}
#[allow(clippy::too_many_arguments)]
fn write_range(
&mut self,
m: &Mdir,
edit: Edit<'_>,
lo: u16,
hi: u16,
block: u32,
rev: u32,
tail: Option<[u32; 2]>,
hard: bool,
gdelta: Option<[u8; 12]>,
) -> Result<(), Error<D::Error>> {
if block >= self.geom.block_count {
return Err(Error::Corrupt("commit target beyond the end of the volume"));
}
self.load(m)?;
self.dev.erase(block).map_err(Error::Io)?;
let bs = self.geom.block_size as usize;
let Self {
dev,
stage,
buf,
geom,
..
} = self;
let src = &buf[..bs];
let keys = mdir::attr_keys(src, m);
let mut c = Commit::new(dev, stage, geom, block, rev)?;
for i in lo..hi {
emit_item(&mut c, src, m, &keys, &edit.item(i), i - lo)?;
}
if let Some(t) = tail {
let ty = if hard {
tag::TYPE_HARDTAIL
} else {
tag::TYPE_SOFTTAIL
};
c.push_pair(tag::Tag::new(ty, tag::ID_NONE, 8), t)?;
}
if let Some(g) = gdelta {
c.push(
tag::Tag::new(tag::TYPE_MOVESTATE, tag::ID_NONE, 12),
&Data::Bytes(&g),
src,
)?;
}
c.finish(geom.fcrc())?;
Ok(())
}
}
const MAX_IDS: usize = 0xff;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CommitOut {
pair: [u32; 2],
placed: Option<Placed>,
}
fn split_point(sizes: &[u16; MAX_IDS + 1], end: u16, limit: usize) -> u16 {
let mut split = 0u16;
while end - split > 1 {
let size: usize = sizes[split as usize..end as usize]
.iter()
.map(|n| *n as usize)
.sum();
if ((end - split) as usize) < MAX_IDS && size <= limit {
break;
}
split += (end - split) / 2;
}
split
}
fn item_size(src: &[u8], m: &Mdir, keys: &[u32; 8], item: &Item<'_>) -> usize {
let (name_len, data_size, sid) = match item {
Item::Copy(sid) | Item::CopyWithAttr(sid, _, _) => (
mdir::name_of(src, m, *sid).map_or(0, |(_, _, len)| len),
mdir::struct_of(src, m, *sid).map_or(0, |s| mdir::struct_size(&s)),
Some(*sid),
),
Item::CopyWithData(sid, data) => (
mdir::name_of(src, m, *sid).map_or(0, |(_, _, len)| len),
mdir::struct_out_size(data),
Some(*sid),
),
Item::New { name, data, .. } => (name.len() as u32, mdir::struct_out_size(data), None),
};
let mut n = 4 + name_len as usize + data_size;
if let Some(sid) = sid {
let edit = match item {
Item::CopyWithAttr(_, key, value) => Some((*key, *value)),
_ => None,
};
if !mdir::no_attrs(keys) || edit.is_some() {
for key in 0..=u8::MAX {
if let Some((k, value)) = edit
&& k == key
{
if let Some(v) = value {
n += 4 + v.len();
}
continue;
}
if mdir::has_key(keys, key)
&& let Some((_, len)) = mdir::attr_of(src, m, sid, key)
{
n += 4 + len as usize;
}
}
}
}
n
}
fn emit_item<D: FlashDriver>(
c: &mut Commit<'_, D>,
src: &[u8],
m: &Mdir,
keys: &[u32; 8],
item: &Item<'_>,
id: u16,
) -> Result<(), Error<D::Error>> {
let sid = match item {
Item::Copy(sid) | Item::CopyWithData(sid, _) | Item::CopyWithAttr(sid, _, _) => {
let (kind, off, len) = mdir::name_of(src, m, *sid).unwrap_or((0, 0, 0));
c.push(
tag::Tag::new(tag::TYPE_NAME | kind as u16, id, len as u16),
&Data::Run { off, len },
src,
)?;
match item {
Item::CopyWithData(_, data) => emit_struct(c, src, id, data)?,
_ => {
if let Some(s) = mdir::struct_of(src, m, *sid) {
emit_source_struct(c, src, id, &s)?;
}
}
}
Some(*sid)
}
Item::New { kind, name, data } => {
c.push(
tag::Tag::new(tag::TYPE_NAME | *kind as u16, id, name.len() as u16),
&Data::Bytes(name),
src,
)?;
emit_struct(c, src, id, data)?;
None
}
};
let Some(sid) = sid else { return Ok(()) };
let edit = match item {
Item::CopyWithAttr(_, key, value) => Some((*key, *value)),
_ => None,
};
if mdir::no_attrs(keys) && edit.is_none() {
return Ok(());
}
for key in 0..=u8::MAX {
if let Some((k, value)) = edit
&& k == key
{
if let Some(v) = value {
c.push(
tag::Tag::new(tag::TYPE_USERATTR | key as u16, id, v.len() as u16),
&Data::Bytes(v),
src,
)?;
}
continue;
}
if mdir::has_key(keys, key)
&& let Some((off, len)) = mdir::attr_of(src, m, sid, key)
{
c.push(
tag::Tag::new(tag::TYPE_USERATTR | key as u16, id, len as u16),
&Data::Run { off, len },
src,
)?;
}
}
Ok(())
}
fn emit_struct<D: FlashDriver>(
c: &mut Commit<'_, D>,
src: &[u8],
id: u16,
data: &StructOut<'_>,
) -> Result<(), Error<D::Error>> {
match data {
StructOut::Dir(p) => c.push_pair(tag::Tag::new(tag::TYPE_DIRSTRUCT, id, 8), *p),
StructOut::Ctz { head, size } => {
c.push_pair(tag::Tag::new(tag::TYPE_CTZSTRUCT, id, 8), [*head, *size])
}
StructOut::Inline(d) => c.push(
tag::Tag::new(tag::TYPE_INLINESTRUCT, id, d.len() as u16),
d,
src,
),
}
}
fn emit_source_struct<D: FlashDriver>(
c: &mut Commit<'_, D>,
src: &[u8],
id: u16,
s: &Struct,
) -> Result<(), Error<D::Error>> {
match s {
Struct::Dir(p) => c.push_pair(tag::Tag::new(tag::TYPE_DIRSTRUCT, id, 8), *p),
Struct::Ctz { head, size } => {
c.push_pair(tag::Tag::new(tag::TYPE_CTZSTRUCT, id, 8), [*head, *size])
}
Struct::Inline { off, len } => c.push(
tag::Tag::new(tag::TYPE_INLINESTRUCT, id, *len as u16),
&Data::Run {
off: *off,
len: *len,
},
src,
),
}
}
impl<D: FlashDriver, const BLOCK: usize, const PROG: usize> Volume<D, BLOCK, PROG> {
#[cfg(feature = "alloc")]
fn alloc_block(&mut self) -> Result<u32, Error<D::Error>> {
if self.used.is_none() {
self.build_used()?;
}
let count = self.geom.block_count;
let start = self.cursor.min(count.saturating_sub(1));
let used = self.used.as_mut().expect("just built");
for i in 0..count {
let b = (start + i) % count;
let w = &mut used[b as usize / 32];
let bit = 1u32 << (b % 32);
if *w & bit == 0 {
*w |= bit;
self.cursor = (b + 1) % count;
return Ok(b);
}
}
Err(Error::NoSpace)
}
#[cfg(not(feature = "alloc"))]
fn alloc_block(&mut self) -> Result<u32, Error<D::Error>> {
let count = self.geom.block_count;
let windows = count.div_ceil(LOOKAHEAD_BLOCKS);
let mut visited = 0u32;
loop {
if !self.look_valid {
self.refill_lookahead()?;
}
while self.look_next < LOOKAHEAD_BLOCKS && self.look_start + self.look_next < count {
let i = self.look_next as usize;
let taken = self.look[i / 32] & (1 << (i % 32)) != 0;
let block = self.look_start + self.look_next;
self.look_next += 1;
if !taken {
self.look[i / 32] |= 1 << (i % 32);
return Ok(block);
}
}
self.look_start += LOOKAHEAD_BLOCKS;
if self.look_start >= count {
self.look_start = 0;
}
self.look_next = 0;
self.look_valid = false;
visited += 1;
if visited > windows {
return Err(Error::NoSpace);
}
}
}
fn alloc_pair(&mut self) -> Result<[u32; 2], Error<D::Error>> {
let a = self.alloc_block()?;
match self.alloc_block() {
Ok(b) => Ok([a, b]),
Err(e) => {
self.free_block(a);
Err(e)
}
}
}
fn mark_used(&mut self, block: u32) {
#[cfg(feature = "alloc")]
if let Some(used) = &mut self.used
&& (block as usize / 32) < used.len()
{
used[block as usize / 32] |= 1 << (block % 32);
}
#[cfg(not(feature = "alloc"))]
if block >= self.look_start && block - self.look_start < LOOKAHEAD_BLOCKS {
let i = (block - self.look_start) as usize;
self.look[i / 32] |= 1 << (i % 32);
}
}
fn free_block(&mut self, block: u32) {
#[cfg(feature = "alloc")]
if let Some(used) = &mut self.used
&& (block as usize / 32) < used.len()
{
used[block as usize / 32] &= !(1 << (block % 32));
self.cursor = self.cursor.min(block);
}
#[cfg(not(feature = "alloc"))]
let _ = block;
}
#[cfg(not(feature = "alloc"))]
fn refill_lookahead(&mut self) -> Result<(), Error<D::Error>> {
let start = self.look_start;
let mut bits = [0u32; LOOKAHEAD_WORDS];
self.traverse(&mut |b| {
if b >= start && b - start < LOOKAHEAD_BLOCKS {
let i = (b - start) as usize;
bits[i / 32] |= 1 << (i % 32);
}
})?;
self.look = bits;
self.look_next = 0;
self.look_valid = true;
Ok(())
}
#[cfg(feature = "alloc")]
fn build_used(&mut self) -> Result<(), Error<D::Error>> {
let words = (self.geom.block_count as usize).div_ceil(32);
let mut bits = ::alloc::vec::Vec::new();
if bits.try_reserve_exact(words).is_err() {
return Err(Error::NoSpace);
}
bits.resize(words, 0u32);
self.traverse(&mut |b| {
if (b as usize / 32) < bits.len() {
bits[b as usize / 32] |= 1 << (b % 32);
}
})?;
self.used = Some(bits);
Ok(())
}
fn traverse(&mut self, mark: &mut dyn FnMut(u32)) -> Result<(), Error<D::Error>> {
if let Some((head, size)) = self.pending_ctz {
self.ctz_traverse(head, size, mark)?;
}
if let Some(p) = self.pending_pair {
self.walk_pairs(p, mark)?;
}
self.walk_pairs(SUPERBLOCK_PAIR, mark)
}
fn walk_pairs(
&mut self,
start: [u32; 2],
mark: &mut dyn FnMut(u32),
) -> Result<(), Error<D::Error>> {
let mut next = Some(start);
let mut hops = 0u32;
while let Some(pair) = next {
let m = self.fetch(pair)?;
mark(m.pair[0]);
mark(m.pair[1]);
for id in 0..m.count {
let bs = self.bs();
let data = mdir::struct_of(&self.buf[..bs], &m, id);
if let Some(Struct::Ctz { head, size }) = data {
self.ctz_traverse(head, size, mark)?;
}
}
next = m.tail;
hops += 1;
if hops > self.geom.block_count {
return Err(Error::Corrupt("cycle in the metadata-pair list"));
}
}
Ok(())
}
pub fn used_blocks(&mut self) -> Result<u32, Error<D::Error>> {
let count = self.geom.block_count;
Ok(self.count_used()?.min(count))
}
#[cfg(feature = "alloc")]
fn count_used(&mut self) -> Result<u32, Error<D::Error>> {
if self.used.is_none() {
self.build_used()?;
}
let used = self.used.as_ref().expect("just built");
Ok(used.iter().map(|w| w.count_ones()).sum())
}
#[cfg(not(feature = "alloc"))]
fn count_used(&mut self) -> Result<u32, Error<D::Error>> {
let count = self.geom.block_count;
let mut total = 0u32;
let mut start = 0u32;
while start < count {
let mut bits = [0u32; LOOKAHEAD_WORDS];
self.traverse(&mut |b| {
if b >= start && b - start < LOOKAHEAD_BLOCKS {
let i = (b - start) as usize;
bits[i / 32] |= 1 << (i % 32);
}
})?;
total += bits.iter().map(|w| w.count_ones()).sum::<u32>();
start += LOOKAHEAD_BLOCKS;
}
Ok(total)
}
pub fn free_blocks(&mut self) -> Result<u32, Error<D::Error>> {
let used = self.used_blocks()?;
Ok(self.geom.block_count.saturating_sub(used))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Contents {
Empty,
Inline { block: u32, off: u32, len: u32 },
Ctz { head: u32, size: u32 },
}
impl Contents {
fn len(&self) -> u32 {
match self {
Contents::Empty => 0,
Contents::Inline { len, .. } => *len,
Contents::Ctz { size, .. } => *size,
}
}
}
pub(super) struct Fill<'a> {
pub old: Contents,
pub at: u32,
pub new: &'a [u8],
}
impl<D: FlashDriver, const BLOCK: usize, const PROG: usize> Volume<D, BLOCK, PROG> {
fn read_pointer(&mut self, block: u32, slot: u32) -> Result<u32, Error<D::Error>> {
if block >= self.geom.block_count {
return Err(Error::Corrupt("file block beyond the end of the volume"));
}
let mut b = [0u8; 4];
self.dev.read(block, 4 * slot, &mut b).map_err(Error::Io)?;
Ok(u32::from_le_bytes(b))
}
fn ctz_find(&mut self, head: u32, size: u32, pos: u32) -> Result<(u32, u32), Error<D::Error>> {
if size == 0 {
return Err(Error::InvalidOffset);
}
let bs = self.geom.block_size;
let (mut current, _) = index::index_of(bs, size - 1);
let (target, off) = index::index_of(bs, pos);
let mut head = head;
while current > target {
let (slot, step) = index::hop(current, target);
head = self.read_pointer(head, slot)?;
current -= step;
}
Ok((head, off))
}
fn ctz_traverse(
&mut self,
head: u32,
size: u32,
mark: &mut dyn FnMut(u32),
) -> Result<(), Error<D::Error>> {
if size == 0 {
return Ok(());
}
let (mut index, _) = index::index_of(self.geom.block_size, size - 1);
let mut head = head;
loop {
mark(head);
if index == 0 {
return Ok(());
}
let count = 2 - (index & 1);
let mut heads = [0u32; 2];
for (i, h) in heads.iter_mut().enumerate().take(count as usize) {
*h = self.read_pointer(head, i as u32)?;
}
for h in heads.iter().take(count as usize - 1) {
mark(*h);
}
head = heads[count as usize - 1];
index -= count;
}
}
fn read_contents(
&mut self,
src: &Contents,
pos: u32,
buf: &mut [u8],
) -> Result<usize, Error<D::Error>> {
if pos >= src.len() || buf.is_empty() {
return Ok(0);
}
match src {
Contents::Empty => Ok(0),
Contents::Inline { block, off, len } => {
let n = buf.len().min((len - pos) as usize);
self.dev
.read(*block, off + pos, &mut buf[..n])
.map_err(Error::Io)?;
Ok(n)
}
Contents::Ctz { head, size } => {
let (block, off) = self.ctz_find(*head, *size, pos)?;
let in_block = (self.geom.block_size - off) as usize;
let n = buf.len().min(in_block).min((size - pos) as usize);
if block >= self.geom.block_count {
return Err(Error::Corrupt("file block beyond the end of the volume"));
}
self.dev
.read(block, off, &mut buf[..n])
.map_err(Error::Io)?;
Ok(n)
}
}
}
fn fill_from(
&mut self,
fill: &Fill<'_>,
off: u32,
out: &mut [u8],
) -> Result<(), Error<D::Error>> {
let new_end = fill.at + fill.new.len() as u32;
let old_size = fill.old.len();
let mut done = 0usize;
while done < out.len() {
let o = off + done as u32;
let want = out.len() - done;
if o >= fill.at && o < new_end {
let s = (o - fill.at) as usize;
let n = want.min(fill.new.len() - s);
out[done..done + n].copy_from_slice(&fill.new[s..s + n]);
done += n;
} else if o < old_size && (o >= new_end || o < fill.at) {
let limit = if o < fill.at {
fill.at.min(old_size) - o
} else {
old_size - o
};
let cap = want.min(limit as usize);
let n = self.read_contents(&fill.old, o, &mut out[done..done + cap])?;
if n == 0 {
return Err(Error::Corrupt("short read while rewriting a file"));
}
done += n;
} else {
let limit = if o < fill.at {
(fill.at - o) as usize
} else {
want
};
let n = want.min(limit);
out[done..done + n].fill(0);
done += n;
}
}
Ok(())
}
fn write_ctz(
&mut self,
index: u32,
prev: Option<u32>,
file_off: u32,
fill: &Fill<'_>,
len: u32,
) -> Result<Option<u32>, Error<D::Error>> {
let saved = self.pending_ctz;
let out = self.write_ctz_blocks(index, prev, file_off, fill, len);
self.pending_ctz = saved;
out
}
fn write_ctz_blocks(
&mut self,
mut index: u32,
mut prev: Option<u32>,
mut file_off: u32,
fill: &Fill<'_>,
len: u32,
) -> Result<Option<u32>, Error<D::Error>> {
let bs = self.geom.block_size;
let mut remaining = len;
while remaining > 0 {
let block = self.alloc_block()?;
let skips = index::pointers(index);
let cap = index::payload(bs, index);
let n = cap.min(remaining);
let start = 4 * skips;
let mut pointers = [0u32; 32];
if skips > 0 {
let mut p = prev.ok_or(Error::Corrupt("skip-list continuation without a head"))?;
for j in 0..skips {
pointers[j as usize] = p;
if j + 1 < skips {
p = self.read_pointer(p, j)?;
}
}
}
self.cached = None;
{
let image = &mut self.buf[..bs as usize];
image.fill(0xff);
for (j, p) in pointers.iter().take(skips as usize).enumerate() {
image[j * 4..j * 4 + 4].copy_from_slice(&p.to_le_bytes());
}
}
self.fill_into_scratch(fill, file_off, start, n)?;
self.dev.erase(block).map_err(Error::Io)?;
let prog = self.geom.prog_size.max(1);
let end = (start + n).next_multiple_of(prog).min(bs) as usize;
self.dev
.prog(block, 0, &self.buf[..end])
.map_err(Error::Io)?;
prev = Some(block);
self.pending_ctz = Some((block, file_off + n));
index += 1;
file_off += n;
remaining -= n;
}
Ok(prev)
}
fn fill_into_scratch(
&mut self,
fill: &Fill<'_>,
off: u32,
at: u32,
len: u32,
) -> Result<(), Error<D::Error>> {
let mut done = 0u32;
let mut tmp = [0u8; 128];
while done < len {
let n = (len - done).min(tmp.len() as u32) as usize;
self.fill_from(fill, off + done, &mut tmp[..n])?;
let dst = (at + done) as usize;
self.buf[dst..dst + n].copy_from_slice(&tmp[..n]);
done += n as u32;
}
Ok(())
}
fn release_ctz(&mut self, head: u32, size: u32, from: u32) -> Result<(), Error<D::Error>> {
#[cfg(feature = "alloc")]
{
if self.used.is_none() {
return Ok(());
}
if size == 0 {
return Ok(());
}
let (mut index, _) = index::index_of(self.geom.block_size, size - 1);
let mut doomed = ::alloc::vec::Vec::new();
self.ctz_traverse(head, size, &mut |b| {
if index >= from {
doomed.push(b);
}
index = index.saturating_sub(1);
})?;
for b in doomed {
self.free_block(b);
}
}
#[cfg(not(feature = "alloc"))]
let _ = (head, size, from);
Ok(())
}
}