use super::dir::DirEntry;
use super::inode::FileKind;
use super::{now, Attr, Fs, ROOT_INO};
use crate::device::BlockDevice;
use crate::error::{Error, Result};
use std::fmt::Write;
pub const CONTROL_DIR_INO: u64 = 0xFFFF_FFFF_FFFF_FF00;
pub const CONTROL_DIR: &str = ".lfs";
pub const CONTROL_FILES: [&str; 4] = ["config", "map.json", "links", "stats"];
const CONFIG_INO: u64 = CONTROL_DIR_INO + 1;
#[must_use]
pub const fn is_control(ino: u64) -> bool {
ino >= CONTROL_DIR_INO && ino <= CONTROL_DIR_INO + CONTROL_FILES.len() as u64
}
#[must_use]
pub fn lookup(parent: u64, name: &str) -> Option<u64> {
if parent == ROOT_INO && name == CONTROL_DIR {
return Some(CONTROL_DIR_INO);
}
if parent == CONTROL_DIR_INO {
return CONTROL_FILES
.iter()
.position(|file| *file == name)
.map(|index| CONTROL_DIR_INO + 1 + index as u64);
}
None
}
#[must_use]
pub fn readdir(ino: u64) -> Option<Vec<DirEntry>> {
(ino == CONTROL_DIR_INO).then(|| {
CONTROL_FILES
.iter()
.enumerate()
.map(|(index, name)| DirEntry {
name: (*name).to_string(),
ino: CONTROL_DIR_INO + 1 + index as u64,
kind: FileKind::File,
})
.collect()
})
}
#[must_use]
pub fn root_entry() -> DirEntry {
DirEntry {
name: CONTROL_DIR.to_string(),
ino: CONTROL_DIR_INO,
kind: FileKind::Dir,
}
}
#[must_use]
pub const fn parent(ino: u64) -> u64 {
if ino == CONTROL_DIR_INO {
ROOT_INO
} else {
CONTROL_DIR_INO
}
}
impl<D: BlockDevice> Fs<D> {
fn control_content(&self, ino: u64) -> Result<Vec<u8>> {
if let Some(pending) = self.control.get(&ino) {
return Ok(pending.clone());
}
let text = match ino - CONTROL_DIR_INO {
1 => self.config.to_lino(),
2 => self.map_json(),
3 => self.links_text(),
4 => self.stats_text(),
_ => return Err(Error::IsDir),
};
Ok(text.into_bytes())
}
#[must_use]
pub fn links_text(&self) -> String {
crate::links::packet::write_lino(&self.links.all())
}
#[must_use]
pub fn stats_text(&self) -> String {
let stats = self.statfs();
let (var_used, var_capacity) = self.links.usage.var.usage();
let mut out = String::new();
let _ = writeln!(out, "(label: \"{}\")", self.sb.label.replace('"', "'"));
let _ = writeln!(out, "(block-size: {})", stats.block_size);
let _ = writeln!(out, "(blocks: {})", stats.blocks);
let _ = writeln!(out, "(free-blocks: {})", stats.free);
let _ = writeln!(out, "(largest-free: {})", stats.largest_free);
let _ = writeln!(out, "(free-extents: {})", self.space.free_extents().count());
let _ = writeln!(out, "(pending-blocks: {})", self.space.pending_blocks());
let _ = writeln!(out, "(files: {})", stats.files);
let _ = writeln!(out, "(links: {})", stats.links);
let _ = writeln!(out, "(layout: {})", self.sb.layout.name());
let _ = writeln!(out, "(name-encoding: {})", self.sb.name_encoding.name());
let _ = writeln!(out, "(journal-blocks: {})", self.sb.journal_blocks);
let _ = writeln!(out, "(seq: {})", self.sb.seq);
let _ = writeln!(out, "(index-bytes: {var_used})");
let _ = writeln!(out, "(index-capacity: {var_capacity})");
out
}
pub(super) fn control_attr(&self, ino: u64) -> Result<Attr> {
let time = now();
let (kind, perm, size) = if ino == CONTROL_DIR_INO {
(FileKind::Dir, 0o555, CONTROL_FILES.len() as u64)
} else {
let perm = if ino == CONFIG_INO && self.is_writable() {
0o644
} else {
0o444
};
(
FileKind::File,
perm,
self.control_content(ino)?.len() as u64,
)
};
let owner = self.inode(ROOT_INO)?;
Ok(Attr {
ino,
kind,
perm,
nlink: if kind == FileKind::Dir { 2 } else { 1 },
uid: owner.uid,
gid: owner.gid,
size,
allocated: 0,
atime: time,
mtime: time,
ctime: time,
})
}
pub(super) fn control_read(&self, ino: u64, offset: u64, size: usize) -> Result<Vec<u8>> {
let content = self.control_content(ino)?;
let start = usize::try_from(offset)
.unwrap_or(usize::MAX)
.min(content.len());
let end = start.saturating_add(size).min(content.len());
Ok(content[start..end].to_vec())
}
pub(super) fn control_write(&mut self, ino: u64, offset: u64, data: &[u8]) -> Result<usize> {
if ino != CONFIG_INO {
return Err(if ino == CONTROL_DIR_INO {
Error::IsDir
} else {
Error::ReadOnly
});
}
self.check_writable()?;
let offset = usize::try_from(offset).map_err(|_| Error::invalid("offset too large"))?;
if offset.saturating_add(data.len()) > 1 << 20 {
return Err(Error::invalid("configuration text too large"));
}
let buffer = self.control.entry(ino).or_default();
if buffer.len() < offset + data.len() {
buffer.resize(offset + data.len(), 0);
}
buffer[offset..offset + data.len()].copy_from_slice(data);
Ok(data.len())
}
pub(super) fn control_flush(&mut self, ino: u64) -> Result<()> {
let Some(buffer) = self.control.remove(&ino) else {
return Ok(());
};
let text =
String::from_utf8(buffer).map_err(|_| Error::invalid("configuration is not UTF-8"))?;
self.apply_config_text(&text)?;
self.commit()
}
}