use std::collections::HashSet;
use std::io::{Read, Seek, SeekFrom};
use crate::constants::*;
use crate::error::{ReadError, ReadResult};
use crate::extent;
use crate::file_tree::InodeNumber;
use crate::reader::Reader;
use crate::types::*;
const MAX_SYMLINK_HOPS: usize = 40;
impl Reader {
pub fn exists(&mut self, path: &str) -> bool {
self.resolve_path(path, true).is_ok()
}
pub fn stat(&mut self, path: &str) -> ReadResult<(InodeNumber, Inode)> {
self.resolve_path(path, true)
}
pub fn stat_no_follow(&mut self, path: &str) -> ReadResult<(InodeNumber, Inode)> {
self.resolve_path(path, false)
}
pub fn list_dir(&mut self, path: &str) -> ReadResult<Vec<String>> {
let (ino_num, inode) = self.stat(path)?;
if !inode.is_dir() {
return Err(ReadError::NotADirectory(path.to_string()));
}
let children = self.children_of(ino_num)?;
Ok(children
.into_iter()
.filter(|(name, _)| name != "." && name != "..")
.map(|(name, _)| name)
.collect())
}
pub fn read_file(
&mut self,
path: &str,
offset: u64,
count: Option<usize>,
) -> ReadResult<Vec<u8>> {
let (ino_num, inode) = self.stat(path)?;
if inode.is_dir() {
return Err(ReadError::IsDirectory(path.to_string()));
}
if !inode.is_reg() {
return Err(ReadError::NotAFile(path.to_string()));
}
let file_size = inode.file_size();
let start = offset.min(file_size);
let max_readable = file_size - start;
let want = match count {
Some(c) => (c as u64).min(max_readable),
None => max_readable,
};
if want == 0 {
return Ok(Vec::new());
}
self.read_from_extents(ino_num, start, want)
}
pub fn read_file_into(
&mut self,
path: &str,
buf: &mut [u8],
offset: u64,
) -> ReadResult<usize> {
let data = self.read_file(path, offset, Some(buf.len()))?;
let n = data.len().min(buf.len());
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
}
}
impl Reader {
fn resolve_path(
&mut self,
path: &str,
follow_symlinks: bool,
) -> ReadResult<(InodeNumber, Inode)> {
let mut components = normalize_path(path);
let mut current: InodeNumber = ROOT_INODE;
let mut parent_stack: Vec<InodeNumber> = Vec::new();
let mut visited: HashSet<InodeNumber> = HashSet::new();
let mut hops: usize = 0;
let mut idx: usize = 0;
while idx < components.len() {
let name = components[idx].clone();
if name == "." {
idx += 1;
continue;
}
if name == ".." {
if current != ROOT_INODE {
if let Some(parent) = parent_stack.pop() {
current = parent;
} else {
let entries = self.children_of(current)?;
if let Some((_, parent_ino)) = entries.iter().find(|(n, _)| n == "..") {
current = *parent_ino;
}
}
}
idx += 1;
continue;
}
let current_inode = self.get_inode(current)?;
if !current_inode.is_dir() {
return Err(ReadError::NotADirectory(name));
}
let entries = self.children_of(current)?;
let child_ino = entries
.iter()
.find(|(n, _)| *n == name)
.map(|(_, ino)| *ino)
.ok_or_else(|| ReadError::PathNotFound(name.clone()))?;
let child_inode = self.get_inode(child_ino)?;
if child_inode.is_link() && follow_symlinks {
if visited.contains(&child_ino) {
return Err(ReadError::SymlinkLoop(path.to_string()));
}
visited.insert(child_ino);
hops += 1;
if hops > MAX_SYMLINK_HOPS {
return Err(ReadError::SymlinkLoop(path.to_string()));
}
let target = self.read_symlink_target(&child_inode, child_ino)?;
if target.is_empty() {
return Err(ReadError::InvalidPath("empty symlink target".to_string()));
}
let target_components = normalize_path(&target);
if target.starts_with('/') {
current = ROOT_INODE;
parent_stack.clear();
let rest: Vec<String> = components[idx + 1..].to_vec();
components = [target_components, rest].concat();
idx = 0;
} else {
let before: Vec<String> = components[..idx].to_vec();
let rest: Vec<String> = components[idx + 1..].to_vec();
components = [before, target_components, rest].concat();
}
} else {
parent_stack.push(current);
current = child_ino;
idx += 1;
}
}
let final_inode = self.get_inode(current)?;
Ok((current, final_inode))
}
}
impl Reader {
fn read_from_extents(
&mut self,
inode_number: InodeNumber,
start: u64,
count: u64,
) -> ReadResult<Vec<u8>> {
let inode = self.get_inode(inode_number)?;
let extents = extent::parse_extents(&inode, self.block_size(), &mut self.file)?;
if extents.is_empty() {
return Ok(Vec::new());
}
let bs = self.block_size();
let req_end = start + count;
let mut out = Vec::with_capacity(count as usize);
let mut logical_offset: u64 = 0;
for (phys_start, phys_end) in &extents {
let extent_bytes = (*phys_end as u64 - *phys_start as u64) * bs;
let logical_end = logical_offset + extent_bytes;
if logical_end <= start {
logical_offset = logical_end;
continue;
}
if logical_offset >= req_end {
break;
}
let overlap_start = start.max(logical_offset);
let overlap_end = req_end.min(logical_end);
let mut remaining = overlap_end - overlap_start;
if remaining == 0 {
logical_offset = logical_end;
continue;
}
let offset_into_extent = overlap_start - logical_offset;
let abs_byte_offset = *phys_start as u64 * bs + offset_into_extent;
self.file.seek(SeekFrom::Start(abs_byte_offset))?;
while remaining > 0 {
let chunk = remaining.min(1 << 20) as usize;
let mut buf = vec![0u8; chunk];
let n = self.file.read(&mut buf)?;
if n == 0 {
break; }
out.extend_from_slice(&buf[..n]);
remaining -= n as u64;
}
logical_offset = logical_end;
if out.len() >= count as usize {
break;
}
}
out.truncate(count as usize);
Ok(out)
}
fn read_symlink_target(
&mut self,
inode: &Inode,
inode_number: InodeNumber,
) -> ReadResult<String> {
let size = inode.file_size();
if size == 0 {
return Ok(String::new());
}
if size < INODE_BLOCK_SIZE as u64 {
let bytes = &inode.block[..size as usize];
return Ok(String::from_utf8_lossy(bytes).into_owned());
}
let data = self.read_from_extents(inode_number, 0, size)?;
Ok(String::from_utf8_lossy(&data).into_owned())
}
}
fn normalize_path(path: &str) -> Vec<String> {
let trimmed = path.strip_prefix('/').unwrap_or(path);
if trimmed.is_empty() {
return Vec::new();
}
trimmed
.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path_absolute() {
assert_eq!(normalize_path("/etc/passwd"), vec!["etc", "passwd"]);
}
#[test]
fn test_normalize_path_relative() {
assert_eq!(normalize_path("etc/passwd"), vec!["etc", "passwd"]);
}
#[test]
fn test_normalize_path_root() {
assert!(normalize_path("/").is_empty());
}
#[test]
fn test_normalize_path_empty() {
assert!(normalize_path("").is_empty());
}
#[test]
fn test_normalize_path_consecutive_slashes() {
assert_eq!(normalize_path("//a///b//"), vec!["a", "b"]);
}
#[test]
fn test_normalize_path_dots() {
assert_eq!(
normalize_path("/a/./b/../c"),
vec!["a", ".", "b", "..", "c"]
);
}
#[test]
fn test_normalize_path_single_component() {
assert_eq!(normalize_path("/file.txt"), vec!["file.txt"]);
assert_eq!(normalize_path("file.txt"), vec!["file.txt"]);
}
#[test]
fn test_normalize_path_trailing_slash() {
assert_eq!(normalize_path("/etc/"), vec!["etc"]);
assert_eq!(normalize_path("/a/b/c/"), vec!["a", "b", "c"]);
}
#[test]
fn test_normalize_path_deeply_nested() {
assert_eq!(
normalize_path("/a/b/c/d/e/f"),
vec!["a", "b", "c", "d", "e", "f"]
);
}
}