1use std::fs;
2use std::path::{Component, Path, PathBuf};
3use std::sync::LazyLock;
4
5use regex::Regex;
6
7use crate::error::{self, TestError};
8
9pub fn split_command_parts(command_line: &str) -> Vec<&str> {
10 static REGEX: LazyLock<Regex> =
11 LazyLock::new(|| Regex::new(r##"r?#"(?:.|\n)*"#|r?"(?:[^"]+)"|\S+"##).expect("regex must be correct"));
12
13 REGEX
14 .find_iter(command_line)
15 .map(|found| {
16 found
17 .as_str()
18 .trim_start_matches("r#\"")
19 .trim_matches('#')
20 .trim_matches('"')
21 })
22 .collect()
23}
24
25#[derive(Debug)]
26pub enum Cmd {
27 Cd(PathBuf),
28 Ls(PathBuf),
29 Mkdir(Vec<PathBuf>),
30 Rm(Vec<PathBuf>),
31 Echo(String, Option<PathBuf>),
32 Cat(PathBuf, Option<PathBuf>),
33}
34
35pub enum CmdResponse {
36 Success,
37 ChangeDirTo(PathBuf),
38 Output(String),
39}
40
41impl Cmd {
42 pub fn parse(root_dir: impl AsRef<Path>, source: &str) -> Result<Self, Vec<&str>> {
43 let root_dir = root_dir.as_ref();
44 let parts = split_command_parts(source);
45
46 let cmd = match &parts[..] {
47 ["cd", path] => Self::Cd(checked_join(root_dir, path)),
48 ["ls", path] => Self::Ls(checked_join(root_dir, path)),
49 ["mkdir", pathes @ ..] => Self::Mkdir(pathes.iter().map(|path| checked_join(root_dir, path)).collect()),
50 ["rm", pathes @ ..] => Self::Rm(pathes.iter().map(|path| checked_join(root_dir, path)).collect()),
51 ["echo", text @ .., ">", path] => Self::Echo(text.to_vec().join(" "), Some(checked_join(root_dir, path))),
52 ["echo", text @ ..] => Self::Echo(text.to_vec().join(" "), None),
53 ["cat", from_path, ">", to_path] => {
54 Self::Cat(checked_join(root_dir, from_path), Some(checked_join(root_dir, to_path)))
55 },
56 ["cat", path] => Self::Cat(checked_join(root_dir, path), None),
57 _ => return Err(parts),
58 };
59 Ok(cmd)
60 }
61
62 pub fn run(self) -> error::Result<CmdResponse> {
63 match self {
64 Self::Cd(path) => cd(path),
65 Self::Ls(path) => ls(path),
66 Self::Mkdir(pathes) => mkdir(pathes),
67 Self::Rm(pathes) => rm(pathes),
68 Self::Echo(text, path) => echo(text, path),
69 Self::Cat(from, to) => cat(from, to),
70 }
71 }
72}
73
74fn checked_join(root: impl AsRef<Path>, subpath: impl AsRef<Path>) -> PathBuf {
75 let root = root.as_ref();
76 let path = normalize_path(root.join(subpath));
77
78 if path.starts_with(root) {
79 path
80 } else {
81 panic!("Path `{}` is not a subpath of `{}`", path.display(), root.display())
82 }
83}
84
85fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
91 let mut components = path.as_ref().components().peekable();
92 let mut normalized = if let Some(comp @ Component::Prefix(..)) = components.peek().cloned() {
93 components.next();
94 PathBuf::from(comp.as_os_str())
95 } else {
96 PathBuf::new()
97 };
98
99 for component in components {
100 match component {
101 Component::Prefix(..) => unreachable!(),
102 Component::RootDir => {
103 normalized.push(Component::RootDir);
104 },
105 Component::CurDir => {},
106 Component::ParentDir => {
107 if normalized.ends_with(Component::ParentDir) {
108 normalized.push(Component::ParentDir);
109 } else {
110 let popped = normalized.pop();
111 if !popped && !normalized.has_root() {
112 normalized.push(Component::ParentDir);
113 }
114 }
115 },
116 Component::Normal(chunk) => {
117 normalized.push(chunk);
118 },
119 }
120 }
121 normalized
122}
123
124fn cd(path: PathBuf) -> error::Result<CmdResponse> {
125 if path.is_dir() {
126 Ok(CmdResponse::ChangeDirTo(path))
127 } else {
128 Err(TestError::Command(format!("Path `{}` is not dir", path.display())))
129 }
130}
131
132fn ls(path: PathBuf) -> error::Result<CmdResponse> {
133 let mut entries = Vec::new();
134
135 for entry in fs::read_dir(&path)? {
136 let entry_path = entry?.path();
137 let entry = entry_path
138 .strip_prefix(&path)
139 .map_err(|_| {
140 TestError::Command(format!(
141 "Could not strip prefix {} for path: {}",
142 path.display(),
143 entry_path.display()
144 ))
145 })?
146 .display()
147 .to_string();
148 entries.push(entry);
149 }
150
151 entries.sort();
152
153 let mut output = entries.join(" ");
154 output.push('\n');
155
156 Ok(CmdResponse::Output(output))
157}
158
159fn mkdir(pathes: Vec<PathBuf>) -> error::Result<CmdResponse> {
160 for path in pathes {
161 fs::create_dir_all(&path)
162 .map_err(|err| TestError::Command(format!("Failed to create directory `{}`: {err}", path.display())))?;
163 }
164 Ok(CmdResponse::Success)
165}
166
167fn rm(pathes: Vec<PathBuf>) -> error::Result<CmdResponse> {
168 for path in pathes {
169 if path.is_dir() {
170 fs::remove_dir_all(&path)
171 .map_err(|err| TestError::Command(format!("Failed to remove directory `{}`: {err}", path.display())))?;
172 } else {
173 fs::remove_file(&path)
174 .map_err(|err| TestError::Command(format!("Failed to remove file `{}`: {err}", path.display())))?;
175 }
176 }
177 Ok(CmdResponse::Success)
178}
179
180fn echo(text: String, path: Option<PathBuf>) -> error::Result<CmdResponse> {
181 if let Some(path) = path {
182 fs::write(&path, text)
183 .map_err(|err| TestError::Command(format!("Failed to write file `{}`: {err}", path.display())))?;
184 Ok(CmdResponse::Success)
185 } else {
186 Ok(CmdResponse::Output(text))
187 }
188}
189
190fn cat(from_path: PathBuf, to_path: Option<PathBuf>) -> error::Result<CmdResponse> {
191 let content = fs::read_to_string(&from_path)
192 .map_err(|err| TestError::Command(format!("Failed to read file `{}`: {err}", from_path.display())))?;
193 echo(content, to_path)
194}