cat_dev/fsemul/pcfs/sata_proto/
change_mode.rs

1//! Definitions, and handlers for the `ChangeMode` packet type.
2//!
3//! Although this implies you can set any arbitrary mode, unfortunately because
4//! the SDK only supported windows, and windows doesn't have full mode strings,
5//! we can only set read only. We're basically a toggle between 0444, and 0666.
6
7use crate::{
8	errors::{CatBridgeError, NetworkParseError},
9	fsemul::{
10		host_filesystem::ResolvedLocation,
11		pcfs::sata_proto::{construct_sata_response, SataPacketHeader},
12		HostFilesystem,
13	},
14};
15use bytes::Bytes;
16use std::{ffi::CStr, fs::set_permissions};
17use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
18
19/// A filesystem error occured.
20const FS_ERROR: u32 = 0xFFF0_FFE0;
21/// An error code to send when a path does not exist.
22///
23/// This is also used in some places that are a bit of a stretch like for
24/// network shares on disk space. The path doesn't exist on a disk, so this
25/// error code is used, even if it's not quite exact.
26const PATH_NOT_EXIST_ERROR: u32 = 0xFFF0_FFE9;
27
28/// A packet to get change read-only state of a path.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct SataChangeModePacketBody {
31	/// The path to query, note that this is not the 'resolved' path which is
32	/// the path to actual read from.
33	///
34	/// Interpolation has a few known ways of being replaced:
35	///
36	/// - `%MLC_EMU_DIR`: `<cafe_sdk>/data/mlc/`
37	/// - `%SLC_EMU_DIR`: `<cafe_sdk>/data/slc/`
38	/// - `%DISC_EMU_DIR`: `<cafe_sdk>/data/disc/`
39	/// - `%SAVE_EMU_DIR`: `<cafe_sdk>/data/save/`
40	/// - `%NETWORK`: <mounted network share path>
41	path: String,
42	/// If we're setting the write mode.
43	set_write_mode: bool,
44}
45
46impl SataChangeModePacketBody {
47	#[must_use]
48	pub fn path(&self) -> &str {
49		self.path.as_str()
50	}
51	#[must_use]
52	pub const fn set_write_mode(&self) -> bool {
53		self.set_write_mode
54	}
55
56	/// Handle a Change Mode Request.
57	///
58	/// This handles setting read only mode one or off.
59	///
60	/// ## Errors
61	///
62	/// If we cannot construct a sata response packet because our data to send
63	/// was somehow too large (this should ideally never happen).
64	pub fn handle(
65		&self,
66		request_header: &SataPacketHeader,
67		host_filesystem: &HostFilesystem,
68	) -> Result<Bytes, CatBridgeError> {
69		let Ok(final_location) = host_filesystem.resolve_path(&self.path) else {
70			return Ok(construct_sata_response(
71				request_header,
72				0,
73				Bytes::from(Vec::from(PATH_NOT_EXIST_ERROR.to_be_bytes())),
74			)?);
75		};
76		let ResolvedLocation::Filesystem(fs_location) = final_location else {
77			todo!("network shares not yet implemented!")
78		};
79		// Path doesn't exist.
80		if !fs_location.canonicalized_is_exact() {
81			return Ok(construct_sata_response(
82				request_header,
83				0,
84				Bytes::from(Vec::from(PATH_NOT_EXIST_ERROR.to_be_bytes())),
85			)?);
86		}
87
88		let Ok(metadata) = fs_location.closest_resolved_path().metadata() else {
89			return Ok(construct_sata_response(
90				request_header,
91				0,
92				Bytes::from(Vec::from(FS_ERROR.to_be_bytes())),
93			)?);
94		};
95
96		// If we're changing to write, we need to do a path check.
97		let mut perms = metadata.permissions();
98		if self.set_write_mode
99			&& !host_filesystem.path_allows_writes(fs_location.closest_resolved_path())
100		{
101			return Ok(construct_sata_response(
102				request_header,
103				0,
104				Bytes::from(Vec::from(FS_ERROR.to_be_bytes())),
105			)?);
106		}
107		perms.set_readonly(!self.set_write_mode);
108		if set_permissions(fs_location.closest_resolved_path(), perms).is_err() {
109			return Ok(construct_sata_response(
110				request_header,
111				0,
112				Bytes::from(Vec::from(FS_ERROR.to_be_bytes())),
113			)?);
114		}
115
116		Ok(construct_sata_response(
117			request_header,
118			0,
119			Bytes::from(vec![0; 4]),
120		)?)
121	}
122}
123
124impl TryFrom<Bytes> for SataChangeModePacketBody {
125	type Error = NetworkParseError;
126
127	fn try_from(value: Bytes) -> Result<Self, Self::Error> {
128		if value.len() < 0x204 {
129			return Err(NetworkParseError::FieldNotLongEnough(
130				"SataChangeMode",
131				"Body",
132				0x204,
133				value.len(),
134				value,
135			));
136		}
137		if value.len() > 0x204 {
138			return Err(NetworkParseError::UnexpectedTrailer(
139				"SataChangeMode",
140				value.slice(0x204..),
141			));
142		}
143
144		let (path_bytes, num) = value.split_at(0x200);
145		let path_c_str =
146			CStr::from_bytes_until_nul(path_bytes).map_err(NetworkParseError::BadCString)?;
147		let write_mode_flags = u32::from_be_bytes([num[0], num[1], num[2], num[3]]);
148		let final_path = path_c_str.to_str()?.to_owned();
149
150		Ok(Self {
151			path: final_path,
152			set_write_mode: write_mode_flags & 0x222 != 0,
153		})
154	}
155}
156
157const SATA_CHANGE_MODE_PACKET_BODY_FIELDS: &[NamedField<'static>] = &[NamedField::new("path")];
158
159impl Structable for SataChangeModePacketBody {
160	fn definition(&self) -> StructDef<'_> {
161		StructDef::new_static(
162			"SataChangeModePacketBody",
163			Fields::Named(SATA_CHANGE_MODE_PACKET_BODY_FIELDS),
164		)
165	}
166}
167
168impl Valuable for SataChangeModePacketBody {
169	fn as_value(&self) -> Value<'_> {
170		Value::Structable(self)
171	}
172
173	fn visit(&self, visitor: &mut dyn Visit) {
174		visitor.visit_named_fields(&NamedValues::new(
175			SATA_CHANGE_MODE_PACKET_BODY_FIELDS,
176			&[Valuable::as_value(&self.path)],
177		));
178	}
179}
180
181#[cfg(test)]
182mod unit_tests {
183	use super::*;
184	use crate::fsemul::host_filesystem::test_helpers::{
185		create_temporary_host_filesystem, join_many,
186	};
187
188	#[tokio::test]
189	pub async fn change_mode_request() {
190		let (tempdir, fs) = create_temporary_host_filesystem().await;
191		let request = SataChangeModePacketBody {
192			path: "/%SLC_EMU_DIR/to-query/file.txt".to_owned(),
193			set_write_mode: false,
194		};
195		let mocked_header = SataPacketHeader {
196			packet_data_len: 0,
197			packet_id: 0,
198			flags: 0,
199			version: 0,
200			timestamp_on_host: 0,
201			pid_on_host: 0,
202		};
203
204		// Okay let's create some files and some sizes in there.
205		let base_dir = join_many(tempdir.path(), ["data", "slc", "to-query"]);
206		tokio::fs::create_dir(&base_dir)
207			.await
208			.expect("Failed to create temporary directory for test!");
209		tokio::fs::write(join_many(&base_dir, ["file.txt"]), vec![0; 1307])
210			.await
211			.expect("Failed to write test file!");
212
213		let mut response = request
214			.handle(&mocked_header, &fs)
215			.expect("Failed to handle change mode!");
216		assert_eq!(response.len(), 4 + 0x20, "Packet is not correct size!");
217		// Okay first chop off the header, we don't care.
218		_ = response.split_to(0x20);
219		assert_eq!(
220			response,
221			Bytes::from(vec![
222				0x00, 0x00, 0x00, 0x00, // RC
223			]),
224		);
225	}
226}