use crate::chunked::ChunkedArray;
use crate::codec::{get_i64, get_u32, get_u64, put_i64, put_u32, put_u64};
use std::collections::BTreeSet;
pub const RECORD_SIZE: usize = 64;
pub const S_IFMT: u32 = 0o170_000;
pub const S_IFREG: u32 = 0o100_000;
pub const S_IFDIR: u32 = 0o040_000;
pub const S_IFLNK: u32 = 0o120_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FileKind {
File,
Dir,
Symlink,
}
impl FileKind {
#[must_use]
pub const fn from_mode(mode: u32) -> Option<Self> {
match mode & S_IFMT {
S_IFREG => Some(Self::File),
S_IFDIR => Some(Self::Dir),
S_IFLNK => Some(Self::Symlink),
_ => None,
}
}
#[must_use]
pub const fn mode_bits(self) -> u32 {
match self {
Self::File => S_IFREG,
Self::Dir => S_IFDIR,
Self::Symlink => S_IFLNK,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Inode {
pub mode: u32,
pub nlink: u32,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub start: u64,
pub blocks: u64,
pub mtime: i64,
pub ctime: i64,
pub atime: i64,
}
impl Inode {
#[must_use]
pub fn read(bytes: &[u8]) -> Self {
Self {
mode: get_u32(bytes, 0),
nlink: get_u32(bytes, 4),
uid: get_u32(bytes, 8),
gid: get_u32(bytes, 12),
size: get_u64(bytes, 16),
start: get_u64(bytes, 24),
blocks: get_u64(bytes, 32),
mtime: get_i64(bytes, 40),
ctime: get_i64(bytes, 48),
atime: get_i64(bytes, 56),
}
}
pub fn write(&self, bytes: &mut [u8]) {
put_u32(bytes, 0, self.mode);
put_u32(bytes, 4, self.nlink);
put_u32(bytes, 8, self.uid);
put_u32(bytes, 12, self.gid);
put_u64(bytes, 16, self.size);
put_u64(bytes, 24, self.start);
put_u64(bytes, 32, self.blocks);
put_i64(bytes, 40, self.mtime);
put_i64(bytes, 48, self.ctime);
put_i64(bytes, 56, self.atime);
}
#[must_use]
pub const fn kind(&self) -> Option<FileKind> {
FileKind::from_mode(self.mode)
}
#[must_use]
pub const fn is_used(&self) -> bool {
self.mode != 0
}
}
#[derive(Clone, Debug)]
pub struct Inodes {
pub array: ChunkedArray,
free: BTreeSet<u64>,
}
impl Inodes {
#[must_use]
pub fn new(block_size: usize) -> Self {
Self::from_array(ChunkedArray::new(RECORD_SIZE, block_size))
}
#[must_use]
pub fn from_array(array: ChunkedArray) -> Self {
let free = (0..array.capacity())
.filter(|&slot| get_u32(array.record(slot), 0) == 0)
.collect();
Self { array, free }
}
#[must_use]
pub fn get(&self, slot: u64) -> Inode {
if slot < self.array.capacity() {
Inode::read(self.array.record(slot))
} else {
Inode::default()
}
}
pub fn set(&mut self, slot: u64, inode: &Inode) {
if self.get(slot) == *inode {
return;
}
inode.write(self.array.record_mut(slot));
if inode.is_used() {
self.free.remove(&slot);
} else {
self.free.insert(slot);
}
}
pub fn allocate(&mut self) -> u64 {
if let Some(slot) = self.free.pop_first() {
return slot;
}
let first = self.array.capacity();
self.array.ensure_capacity(first + 1);
self.free.extend(first + 1..self.array.capacity());
first
}
pub fn free(&mut self, slot: u64) {
self.set(slot, &Inode::default());
}
#[must_use]
pub fn used(&self) -> u64 {
self.array.capacity() - self.free.len() as u64
}
#[must_use]
pub const fn capacity(&self) -> u64 {
self.array.capacity()
}
pub fn each(&self) -> impl Iterator<Item = (u64, Inode)> + '_ {
(0..self.array.capacity())
.map(|slot| (slot, self.get(slot)))
.filter(|(_, inode)| inode.is_used())
}
pub fn shrink(&mut self, space: &mut crate::space::Space) {
let mut keep = self.array.capacity();
while keep > 1 && self.free.contains(&(keep - 1)) {
keep -= 1;
}
let per_chunk = self.array.per_chunk() as u64;
let keep = keep.div_ceil(per_chunk) * per_chunk;
if keep < self.array.capacity() {
self.array.truncate_chunks(keep, space);
self.free.retain(|&slot| slot < keep);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn records_round_trip_and_slots_are_reused() {
let mut inodes = Inodes::new(1024);
let a = inodes.allocate();
let b = inodes.allocate();
assert_eq!((a, b), (0, 1));
let inode = Inode {
mode: S_IFREG | 0o644,
nlink: 1,
size: 5,
start: 9,
blocks: 1,
mtime: -3,
..Inode::default()
};
inodes.set(b, &inode);
assert_eq!(inodes.get(b), inode);
assert_eq!(inodes.get(b).kind(), Some(FileKind::File));
inodes.free(b);
assert_eq!(inodes.allocate(), 1, "freed slots are reused first");
inodes.free(a);
let reloaded = Inodes::from_array(inodes.array.clone());
assert_eq!(reloaded.used(), 0);
}
}