use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EnhancedFileSystemEntry {
Directory {
mode: u32,
uid: u32,
gid: u32,
},
File {
content: String,
mode: u32,
uid: u32,
gid: u32,
mtime: Option<i64>,
},
}
impl EnhancedFileSystemEntry {
pub fn mode(&self) -> u32 {
match self {
Self::Directory { mode, .. } | Self::File { mode, .. } => *mode,
}
}
pub fn uid(&self) -> u32 {
match self {
Self::Directory { uid, .. } | Self::File { uid, .. } => *uid,
}
}
pub fn gid(&self) -> u32 {
match self {
Self::Directory { gid, .. } | Self::File { gid, .. } => *gid,
}
}
pub fn is_directory(&self) -> bool {
matches!(self, Self::Directory { .. })
}
pub fn is_file(&self) -> bool {
matches!(self, Self::File { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnhancedState {
pub env: HashMap<String, String>,
pub cwd: PathBuf,
pub stdout: Vec<String>,
pub stderr: Vec<String>,
pub exit_code: i32,
pub filesystem: HashMap<PathBuf, EnhancedFileSystemEntry>,
pub euid: u32,
pub egid: u32,
pub groups: Vec<u32>,
}
impl Default for EnhancedState {
fn default() -> Self {
let mut filesystem = HashMap::new();
filesystem.insert(
PathBuf::from("/"),
EnhancedFileSystemEntry::Directory {
mode: 0o755,
uid: 0,
gid: 0,
},
);
Self {
env: HashMap::new(),
cwd: PathBuf::from("/"),
stdout: Vec::new(),
stderr: Vec::new(),
exit_code: 0,
filesystem,
euid: 0, egid: 0, groups: vec![0], }
}
}
impl EnhancedState {
pub fn new() -> Self {
Self::default()
}
pub fn new_user(uid: u32, gid: u32) -> Self {
Self {
euid: uid,
egid: gid,
groups: vec![gid],
..Default::default()
}
}
pub fn set_env(&mut self, name: String, value: String) {
self.env.insert(name, value);
}
pub fn get_env(&self, name: &str) -> Option<&String> {
self.env.get(name)
}
pub fn change_directory(&mut self, path: PathBuf) -> Result<(), String> {
match self.filesystem.get(&path) {
Some(entry) if entry.is_directory() => {
if self.can_execute(&path) {
self.cwd = path;
self.exit_code = 0;
Ok(())
} else {
self.stderr
.push(format!("cd: {}: Permission denied", path.display()));
self.exit_code = 1;
Err("Permission denied".to_string())
}
}
Some(_) => {
self.stderr
.push(format!("cd: {}: Not a directory", path.display()));
self.exit_code = 1;
Err("Not a directory".to_string())
}
None => {
self.stderr
.push(format!("cd: {}: No such file or directory", path.display()));
self.exit_code = 1;
Err("No such file or directory".to_string())
}
}
}
pub fn can_read(&self, path: &PathBuf) -> bool {
self.check_permission(path, 0o4) }
pub fn can_write(&self, path: &PathBuf) -> bool {
self.check_permission(path, 0o2) }
pub fn can_execute(&self, path: &PathBuf) -> bool {
self.check_permission(path, 0o1) }
fn check_permission(&self, path: &PathBuf, perm_bit: u32) -> bool {
match self.filesystem.get(path) {
Some(entry) => {
let mode = entry.mode();
let uid = entry.uid();
let gid = entry.gid();
if self.euid == 0 {
return true;
}
if uid == self.euid {
return (mode >> 6) & perm_bit != 0;
}
if gid == self.egid || self.groups.contains(&gid) {
return (mode >> 3) & perm_bit != 0;
}
mode & perm_bit != 0
}
None => {
if let Some(parent) = path.parent() {
self.can_write(&parent.to_path_buf())
} else {
false
}
}
}
}
pub fn create_directory_safe(&mut self, path: PathBuf, mode: u32) -> Result<(), String> {
match self.filesystem.get(&path) {
Some(EnhancedFileSystemEntry::Directory { .. }) => {
self.exit_code = 0;
return Ok(());
}
Some(EnhancedFileSystemEntry::File { .. }) => {
self.stderr.push(format!(
"mkdir: cannot create directory '{}': File exists",
path.display()
));
self.exit_code = 1;
return Err("File exists".to_string());
}
None => {
}
}
if let Some(parent) = path.parent() {
let parent_path = parent.to_path_buf();
if !self.can_write(&parent_path) {
self.stderr.push(format!(
"mkdir: cannot create directory '{}': Permission denied",
path.display()
));
self.exit_code = 1;
return Err("Permission denied".to_string());
}
if !self.filesystem.contains_key(&parent_path) {
self.create_directory_safe(parent_path, 0o755)?;
}
}
self.filesystem.insert(
path.clone(),
EnhancedFileSystemEntry::Directory {
mode,
uid: self.euid,
gid: self.egid,
},
);
self.exit_code = 0;
Ok(())
}
pub fn write_file(&mut self, path: PathBuf, content: String, mode: u32) -> Result<(), String> {
if self.filesystem.contains_key(&path) {
if !self.can_write(&path) {
self.stderr
.push(format!("write: {}: Permission denied", path.display()));
self.exit_code = 1;
return Err("Permission denied".to_string());
}
} else {
if let Some(parent) = path.parent() {
let parent_path = parent.to_path_buf();
if !self.can_write(&parent_path) {
self.stderr.push(format!(
"write: cannot create file '{}': Permission denied",
path.display()
));
self.exit_code = 1;
return Err("Permission denied".to_string());
}
}
}
self.filesystem.insert(
path,
EnhancedFileSystemEntry::File {
content,
mode,
uid: self.euid,
gid: self.egid,
mtime: Some(0), },
);
self.exit_code = 0;
Ok(())
}
pub fn read_file(&mut self, path: &PathBuf) -> Result<String, String> {
if !self.can_read(path) {
self.stderr
.push(format!("cat: {}: Permission denied", path.display()));
self.exit_code = 1;
return Err("Permission denied".to_string());
}
match self.filesystem.get(path) {
Some(EnhancedFileSystemEntry::File { content, .. }) => {
self.exit_code = 0;
Ok(content.clone())
}
Some(EnhancedFileSystemEntry::Directory { .. }) => {
self.stderr
.push(format!("cat: {}: Is a directory", path.display()));
self.exit_code = 1;
Err("Is a directory".to_string())
}
None => {
self.stderr.push(format!(
"cat: {}: No such file or directory",
path.display()
));
self.exit_code = 1;
Err("No such file or directory".to_string())
}
}
}
pub fn write_stdout(&mut self, content: String) {
self.stdout.push(content);
self.exit_code = 0;
}
pub fn write_stderr(&mut self, content: String) {
self.stderr.push(content);
}
pub fn is_equivalent(&self, other: &Self) -> bool {
self.env == other.env
&& self.cwd == other.cwd
&& self.exit_code == other.exit_code
&& self.filesystem == other.filesystem
&& self.stdout == other.stdout
&& self.stderr == other.stderr
&& self.euid == other.euid
&& self.egid == other.egid
&& self.groups == other.groups
}
pub fn test_state() -> Self {
let mut state = Self::new_user(1000, 1000);
state.filesystem.insert(
PathBuf::from("/tmp"),
EnhancedFileSystemEntry::Directory {
mode: 0o1777, uid: 0,
gid: 0,
},
);
state.filesystem.insert(
PathBuf::from("/home"),
EnhancedFileSystemEntry::Directory {
mode: 0o755,
uid: 0,
gid: 0,
},
);
state.filesystem.insert(
PathBuf::from("/home/user"),
EnhancedFileSystemEntry::Directory {
mode: 0o755,
uid: 1000,
gid: 1000,
},
);
state.filesystem.insert(
PathBuf::from("/opt"),
EnhancedFileSystemEntry::Directory {
mode: 0o755,
uid: 0,
gid: 0,
},
);
state.set_env("PATH".to_string(), "/usr/bin:/bin".to_string());
state.set_env("HOME".to_string(), "/home/user".to_string());
state.set_env("USER".to_string(), "user".to_string());
state.set_env("UID".to_string(), "1000".to_string());
state
}
}
include!("enhanced_state_part2_incl2.rs");