cat_dev/fsemul/pcfs/sata_proto/
remove.rs1use crate::{
8 errors::{CatBridgeError, FSError, NetworkParseError},
9 fsemul::{
10 host_filesystem::ResolvedLocation,
11 pcfs::sata_proto::{construct_sata_response, SataPacketHeader},
12 HostFilesystem,
13 },
14};
15use bytes::{BufMut, Bytes, BytesMut};
16use std::{
17 ffi::{CStr, OsStr, OsString},
18 path::PathBuf,
19};
20use tokio::fs::{create_dir_all, read_link, remove_dir_all, remove_file, rename};
21use tracing::error;
22use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
23use walkdir::WalkDir;
24
25const FS_ERROR: u32 = 0xFFF0_FFE0;
27const PATH_NOT_EXIST_ERROR: u32 = 0xFFF0_FFE9;
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct SataRemovePacketBody {
37 path: String,
48}
49
50impl SataRemovePacketBody {
51 #[must_use]
52 pub fn path(&self) -> &str {
53 self.path.as_str()
54 }
55
56 #[allow(
63 clippy::collapsible_else_if,
65 )]
66 pub async fn handle(
67 &self,
68 request_header: &SataPacketHeader,
69 actually_do_remove: bool,
70 host_filesystem: &HostFilesystem,
71 ) -> Result<Bytes, CatBridgeError> {
72 let Ok(final_location) = host_filesystem.resolve_path(&self.path) else {
73 return Self::construct_error(request_header, PATH_NOT_EXIST_ERROR);
74 };
75 let ResolvedLocation::Filesystem(fs_location) = final_location else {
76 todo!("network shares not yet implemented!")
77 };
78
79 if fs_location.resolved_path().exists() {
80 if actually_do_remove {
81 if fs_location.resolved_path().is_file() {
82 if let Err(cause) = remove_file(fs_location.resolved_path()).await {
83 error!(
84 ?cause,
85 path = %fs_location.resolved_path().display(),
86 "Failed to remove file as requested by PCFS.",
87 );
88 return Self::construct_error(request_header, FS_ERROR);
89 }
90 } else if fs_location.resolved_path().is_dir() {
91 if let Err(cause) = remove_dir_all(fs_location.resolved_path()).await {
92 error!(
93 ?cause,
94 path = %fs_location.resolved_path().display(),
95 "Failed to remove directory as requested by PCFS."
96 );
97 return Self::construct_error(request_header, FS_ERROR);
98 }
99 } else {
100 return Self::construct_error(request_header, FS_ERROR);
101 }
102 } else {
103 if fs_location.resolved_path().is_file() {
104 let mut new_filename = fs_location
108 .resolved_path()
109 .file_name()
110 .unwrap_or_default()
111 .to_owned();
112 new_filename.push(OsStr::new(".rm"));
113 let mut new_path = fs_location.resolved_path().clone();
114 new_path.pop();
115 new_path.push(new_filename);
116
117 if let Err(cause) = rename(fs_location.resolved_path(), new_path).await {
118 error!(
119 ?cause,
120 path = %fs_location.resolved_path().display(),
121 "Failed to rename file (as opposed to remove) as requested by PCFS."
122 );
123 return Self::construct_error(request_header, FS_ERROR);
124 }
125 } else if fs_location.resolved_path().is_dir() {
126 if let Err(cause) = Self::rename_dir(fs_location.resolved_path()).await {
127 error!(
128 ?cause,
129 path = %fs_location.resolved_path().display(),
130 "Failed to rename folder (as opposed to remove) as requested by PCFS."
131 );
132 return Self::construct_error(request_header, FS_ERROR);
133 }
134 } else {
135 return Self::construct_error(request_header, FS_ERROR);
136 }
137 }
138 }
139
140 Ok(construct_sata_response(
141 request_header,
142 0,
143 BytesMut::zeroed(4).freeze(),
144 )?)
145 }
146
147 async fn rename_dir(old_path: &PathBuf) -> Result<(), FSError> {
157 let mut new_filename = old_path.file_name().unwrap_or_default().to_owned();
158 new_filename.push(OsStr::new(".rm"));
159 let mut new_path = old_path.clone();
160 new_path.pop();
161 new_path.push(new_filename);
162 let old_path_bytes = old_path.as_os_str().as_encoded_bytes();
163 let new_path_as_str_bytes = new_path.as_os_str().as_encoded_bytes();
164
165 create_dir_all(&new_path).await?;
166 for result in WalkDir::new(old_path)
167 .follow_links(false)
168 .follow_root_links(false)
169 {
170 let rpb = result?.into_path();
171 let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
172 let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
173 new_bytes.extend_from_slice(new_path_as_str_bytes);
174 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
175 let as_new_path =
176 PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
177
178 if rpb.is_symlink() {
179 let mut resolved_path = read_link(&rpb).await?;
180 {
181 let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
183 if os_str_for_resolved.starts_with(old_path_bytes) {
184 let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
185 new_bytes.extend_from_slice(new_path_as_str_bytes);
186 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
187 resolved_path = PathBuf::from(unsafe {
188 OsString::from_encoded_bytes_unchecked(new_bytes)
189 });
190 }
191 }
192
193 #[cfg(unix)]
194 {
195 use std::os::unix::fs::symlink;
196 symlink(resolved_path, &as_new_path)?;
197 }
198
199 #[cfg(target_os = "windows")]
200 {
201 use std::os::windows::fs::{symlink_dir, symlink_file};
202
203 if resolved_path.is_dir() {
204 symlink_dir(resolved_path, &as_new_path)?;
205 } else {
206 symlink_file(resolved_path, &as_new_path)?;
207 }
208 }
209 } else if rpb.is_file() {
210 rename(&rpb, &as_new_path).await?;
211 } else if rpb.is_dir() {
212 create_dir_all(as_new_path).await?;
213 }
214 }
215
216 remove_dir_all(old_path).await?;
217 Ok(())
218 }
219
220 fn construct_error(
221 packet_header: &SataPacketHeader,
222 error_code: u32,
223 ) -> Result<Bytes, CatBridgeError> {
224 let mut buff = BytesMut::with_capacity(8);
225 buff.put_u32(error_code);
226 buff.put_u32(0);
227
228 Ok(construct_sata_response(packet_header, 0, buff.freeze())?)
229 }
230}
231
232impl TryFrom<Bytes> for SataRemovePacketBody {
233 type Error = NetworkParseError;
234
235 fn try_from(value: Bytes) -> Result<Self, Self::Error> {
236 if value.len() < 0x200 {
237 return Err(NetworkParseError::FieldNotLongEnough(
238 "SataRemove",
239 "Body",
240 0x200,
241 value.len(),
242 value,
243 ));
244 }
245 if value.len() > 0x200 {
246 return Err(NetworkParseError::UnexpectedTrailer(
247 "SataRemove",
248 value.slice(0x200..),
249 ));
250 }
251
252 let path_c_str =
253 CStr::from_bytes_until_nul(&value).map_err(NetworkParseError::BadCString)?;
254
255 Ok(Self {
256 path: path_c_str.to_str()?.to_owned(),
257 })
258 }
259}
260
261const SATA_REMOVE_PACKET_BODY_FIELDS: &[NamedField<'static>] = &[NamedField::new("path")];
262
263impl Structable for SataRemovePacketBody {
264 fn definition(&self) -> StructDef<'_> {
265 StructDef::new_static(
266 "SataRemovePacketBody",
267 Fields::Named(SATA_REMOVE_PACKET_BODY_FIELDS),
268 )
269 }
270}
271
272impl Valuable for SataRemovePacketBody {
273 fn as_value(&self) -> Value<'_> {
274 Value::Structable(self)
275 }
276
277 fn visit(&self, visitor: &mut dyn Visit) {
278 visitor.visit_named_fields(&NamedValues::new(
279 SATA_REMOVE_PACKET_BODY_FIELDS,
280 &[Valuable::as_value(&self.path)],
281 ));
282 }
283}
284
285#[cfg(test)]
286mod unit_tests {
287 use super::*;
288 use crate::fsemul::host_filesystem::test_helpers::{
289 create_temporary_host_filesystem, join_many,
290 };
291
292 #[tokio::test]
293 pub async fn test_real_removal() {
294 let (tempdir, fs) = create_temporary_host_filesystem().await;
295
296 let base_dir = join_many(tempdir.path(), ["a", "b", "c"]);
297 tokio::fs::create_dir_all(&base_dir)
298 .await
299 .expect("Failed to create temporary directory for test!");
300 let file_path = join_many(&base_dir, ["file.txt"]);
301 tokio::fs::write(&file_path, vec![0; 1307])
302 .await
303 .expect("Failed to write test file!");
304
305 let request = SataRemovePacketBody {
306 path: base_dir
307 .to_str()
308 .expect("Test paths must be UTF-8")
309 .to_owned(),
310 };
311 let mocked_header = SataPacketHeader {
312 packet_data_len: 0,
313 packet_id: 0,
314 flags: 0,
315 version: 0,
316 timestamp_on_host: 0,
317 pid_on_host: 0,
318 };
319
320 let bytes = request
321 .handle(&mocked_header, true, &fs)
322 .await
323 .expect("Failed to handle removal that was fake!");
324
325 assert!(
326 !base_dir.exists(),
327 "Base directory still exists post 'removal', response:\n\n {:02X?}\n",
328 bytes,
329 );
330 }
331
332 #[tokio::test]
333 pub async fn test_fake_removal() {
334 let (tempdir, fs) = create_temporary_host_filesystem().await;
335
336 let base_dir = join_many(tempdir.path(), ["a", "b", "c"]);
337 tokio::fs::create_dir_all(&base_dir)
338 .await
339 .expect("Failed to create temporary directory for test!");
340 let file_path = join_many(&base_dir, ["file.txt"]);
341 tokio::fs::write(&file_path, vec![0; 1307])
342 .await
343 .expect("Failed to write test file!");
344
345 let inner_path = join_many(tempdir.path(), ["a", "b", "c", "d", "e"]);
346 tokio::fs::create_dir_all(&inner_path)
347 .await
348 .expect("Failed to create temporary directory for test!");
349
350 let directory_to_symlink = join_many(tempdir.path(), ["data", "slc"]);
351 let dir_path_to_symlink = join_many(tempdir.path(), ["a", "b", "c", "d", "e", "f"]);
352
353 let file_path_to_symlink = join_many(
354 tempdir.path(),
355 ["a", "b", "c", "d", "e", "symlinked-file.txt"],
356 );
357
358 #[cfg(unix)]
359 {
360 use std::os::unix::fs::symlink;
361
362 symlink(&directory_to_symlink, &dir_path_to_symlink)
363 .expect("Failed to symlink directory!");
364 symlink(&file_path, &file_path_to_symlink).expect("Failed to symlink file!");
365 }
366
367 #[cfg(target_os = "windows")]
368 {
369 use std::os::windows::fs::{symlink_dir, symlink_file};
370
371 symlink_dir(&directory_to_symlink, &dir_path_to_symlink)
372 .expect("Failed to symlink directory!");
373 symlink_file(&file_path, &file_path_to_symlink).expect("Failed to symlink file!");
374 }
375
376 let request = SataRemovePacketBody {
377 path: base_dir
378 .to_str()
379 .expect("Test paths must be UTF-8")
380 .to_owned(),
381 };
382 let mocked_header = SataPacketHeader {
383 packet_data_len: 0,
384 packet_id: 0,
385 flags: 0,
386 version: 0,
387 timestamp_on_host: 0,
388 pid_on_host: 0,
389 };
390
391 let _ = request
392 .handle(&mocked_header, false, &fs)
393 .await
394 .expect("Failed to handle removal that was fake!");
395 let renamed_dir = join_many(tempdir.path(), ["a", "b", "c.rm"]);
396
397 assert!(
398 !base_dir.exists(),
399 "Base directory still exists post 'removal'",
400 );
401 assert!(renamed_dir.exists(), "Renamed directory doesn't exist?");
402 }
403}