cat_dev/fsemul/pcfs/sata_proto/
create_directory.rs

1//! Definitions, and handlers for the `CreateDir` packet type.
2//!
3//! This creates folders on disk, and nothing more.
4
5use crate::{
6	errors::{CatBridgeError, NetworkParseError},
7	fsemul::{
8		host_filesystem::ResolvedLocation,
9		pcfs::sata_proto::{construct_sata_response, SataPacketHeader},
10		HostFilesystem,
11	},
12};
13use bytes::{BufMut, Bytes, BytesMut};
14use std::ffi::CStr;
15use tokio::fs::{create_dir_all, set_permissions};
16use tracing::error;
17use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
18
19/// A filesystem error occured.
20const FS_ERROR: u32 = 0xFFF0_FFE0;
21
22/// A packet to create a new directory.
23///
24/// This will create a directory if it does not exist.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct SataCreateDirectoryPacketBody {
27	/// The path to create, note that this is not the 'resolved' path which is
28	/// the path to actual read from.
29	///
30	/// Interpolation has a few known ways of being replaced:
31	///
32	/// - `%MLC_EMU_DIR`: `<cafe_sdk>/data/mlc/`
33	/// - `%SLC_EMU_DIR`: `<cafe_sdk>/data/slc/`
34	/// - `%DISC_EMU_DIR`: `<cafe_sdk>/data/disc/`
35	/// - `%SAVE_EMU_DIR`: `<cafe_sdk>/data/save/`
36	/// - `%NETWORK`: <mounted network share path>
37	path: String,
38	/// If we're setting the write mode.
39	set_write_mode: bool,
40}
41
42impl SataCreateDirectoryPacketBody {
43	#[must_use]
44	pub fn path(&self) -> &str {
45		self.path.as_str()
46	}
47	#[must_use]
48	pub const fn set_write_mode(&self) -> bool {
49		self.set_write_mode
50	}
51
52	/// Handle creating a directory upon request.
53	///
54	/// ## Errors
55	///
56	/// If we cannot construct a sata response packet because our data to send
57	/// was somehow too large (this should ideally never happen).
58	pub async fn handle(
59		&self,
60		request_header: &SataPacketHeader,
61		host_filesystem: &HostFilesystem,
62	) -> Result<Bytes, CatBridgeError> {
63		let Ok(final_location) = host_filesystem.resolve_path(&self.path) else {
64			return Self::construct_error(request_header, FS_ERROR);
65		};
66		let ResolvedLocation::Filesystem(fs_location) = final_location else {
67			todo!("network shares not yet implemented!")
68		};
69
70		// Do path check for writes.
71		if self.set_write_mode
72			&& !host_filesystem.path_allows_writes(fs_location.closest_resolved_path())
73		{
74			return Self::construct_error(request_header, FS_ERROR);
75		}
76
77		if !fs_location.resolved_path().exists() {
78			if let Err(cause) = create_dir_all(fs_location.resolved_path()).await {
79				error!(
80				  ?cause,
81				  path = %fs_location.resolved_path().display(),
82				  "Failed to create directory for PCFS.",
83				);
84				return Self::construct_error(request_header, FS_ERROR);
85			}
86		}
87		// Mark as read only.
88		if !self.set_write_mode {
89			let Ok(metadata) = fs_location.resolved_path().metadata() else {
90				return Self::construct_error(request_header, FS_ERROR);
91			};
92			let mut perms = metadata.permissions();
93			perms.set_readonly(true);
94			if set_permissions(fs_location.resolved_path(), perms)
95				.await
96				.is_err()
97			{
98				return Self::construct_error(request_header, FS_ERROR);
99			}
100		}
101
102		Ok(construct_sata_response(
103			request_header,
104			0,
105			BytesMut::zeroed(4).freeze(),
106		)?)
107	}
108
109	fn construct_error(
110		packet_header: &SataPacketHeader,
111		error_code: u32,
112	) -> Result<Bytes, CatBridgeError> {
113		let mut buff = BytesMut::with_capacity(8);
114		buff.put_u32(error_code);
115		buff.put_u32(0);
116
117		Ok(construct_sata_response(packet_header, 0, buff.freeze())?)
118	}
119}
120
121impl TryFrom<Bytes> for SataCreateDirectoryPacketBody {
122	type Error = NetworkParseError;
123
124	fn try_from(value: Bytes) -> Result<Self, Self::Error> {
125		if value.len() < 0x204 {
126			return Err(NetworkParseError::FieldNotLongEnough(
127				"SataCreateDirectory",
128				"Body",
129				0x204,
130				value.len(),
131				value,
132			));
133		}
134		if value.len() > 0x204 {
135			return Err(NetworkParseError::UnexpectedTrailer(
136				"SataCreateDirectory",
137				value.slice(0x204..),
138			));
139		}
140
141		let path_c_str =
142			CStr::from_bytes_until_nul(&value[..0x200]).map_err(NetworkParseError::BadCString)?;
143		let mode = u32::from_be_bytes([value[0x200], value[0x201], value[0x202], value[0x203]]);
144
145		Ok(Self {
146			path: path_c_str.to_str()?.to_owned(),
147			set_write_mode: mode & 0x222 != 0,
148		})
149	}
150}
151
152const SATA_CREATE_DIRECTORY_PACKET_BODY_FIELDS: &[NamedField<'static>] = &[NamedField::new("path")];
153
154impl Structable for SataCreateDirectoryPacketBody {
155	fn definition(&self) -> StructDef<'_> {
156		StructDef::new_static(
157			"SataCreateDirectoryPacketBody",
158			Fields::Named(SATA_CREATE_DIRECTORY_PACKET_BODY_FIELDS),
159		)
160	}
161}
162
163impl Valuable for SataCreateDirectoryPacketBody {
164	fn as_value(&self) -> Value<'_> {
165		Value::Structable(self)
166	}
167
168	fn visit(&self, visitor: &mut dyn Visit) {
169		visitor.visit_named_fields(&NamedValues::new(
170			SATA_CREATE_DIRECTORY_PACKET_BODY_FIELDS,
171			&[Valuable::as_value(&self.path)],
172		));
173	}
174}
175
176#[cfg(test)]
177mod unit_tests {
178	use super::*;
179	use crate::fsemul::host_filesystem::test_helpers::{
180		create_temporary_host_filesystem, join_many,
181	};
182
183	#[tokio::test]
184	pub async fn test_simple_create_directory() {
185		let (tempdir, fs) = create_temporary_host_filesystem().await;
186
187		let base_dir = join_many(tempdir.path(), ["a", "b", "c"]);
188
189		let request = SataCreateDirectoryPacketBody {
190			path: base_dir
191				.to_str()
192				.expect("Test paths must be UTF-8")
193				.to_owned(),
194			set_write_mode: true,
195		};
196		let mocked_header = SataPacketHeader {
197			packet_data_len: 0,
198			packet_id: 0,
199			flags: 0,
200			version: 0,
201			timestamp_on_host: 0,
202			pid_on_host: 0,
203		};
204
205		assert!(!base_dir.exists());
206		let _ = request
207			.handle(&mocked_header, &fs)
208			.await
209			.expect("Failed to handle create directory!");
210		assert!(base_dir.exists());
211	}
212}