cat_dev/fsemul/pcfs/sata_proto/
open_file.rs

1//! Definitions, and handlers for the `OpenFile` packet type.
2//!
3//! This doesn't _Read_ any data out of the file, but merely opens it for
4//! reading, or writing.
5
6use crate::{
7	errors::{CatBridgeError, NetworkParseError},
8	fsemul::{
9		host_filesystem::ResolvedLocation,
10		pcfs::{
11			errors::SataProtocolError,
12			sata_proto::{construct_sata_response, SataCommandInfo, SataPacketHeader},
13		},
14		HostFilesystem,
15	},
16};
17use bytes::{BufMut, Bytes, BytesMut};
18use std::ffi::CStr;
19use tokio::fs::{set_permissions, OpenOptions};
20use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
21
22/// A filesystem error occured.
23const FS_ERROR: u32 = 0xFFF0_FFE0;
24/// An error code to send when a path does not exist.
25///
26/// This is also used in some places that are a bit of a stretch like for
27/// network shares on disk space. The path doesn't exist on a disk, so this
28/// error code is used, even if it's not quite exact.
29const PATH_NOT_EXIST_ERROR: u32 = 0xFFF0_FFE9;
30
31/// A packet to open a file.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct SataOpenFilePacketBody {
34	/// The current mode string.
35	mode_string: String,
36	/// The path to query, note that this is not the 'resolved' path which is
37	/// the path to actual read from.
38	///
39	/// Interpolation has a few known ways of being replaced:
40	///
41	/// - `%MLC_EMU_DIR`: `<cafe_sdk>/data/mlc/`
42	/// - `%SLC_EMU_DIR`: `<cafe_sdk>/data/slc/`
43	/// - `%DISC_EMU_DIR`: `<cafe_sdk>/data/disc/`
44	/// - `%SAVE_EMU_DIR`: `<cafe_sdk>/data/save/`
45	/// - `%NETWORK`: <mounted network share path>
46	path: String,
47}
48
49impl SataOpenFilePacketBody {
50	#[must_use]
51	pub fn mode(&self) -> &str {
52		self.mode_string.as_str()
53	}
54	#[must_use]
55	pub fn path(&self) -> &str {
56		self.path.as_str()
57	}
58
59	/// Handle opening a file upon request.
60	///
61	/// ## Errors
62	///
63	/// If we cannot construct a sata response packet because our data to send
64	/// was somehow too large (this should ideally never happen).
65	#[allow(
66		// Yes clippy, this is what I want, which is why i wrote it.
67		clippy::permissions_set_readonly_false,
68	)]
69	pub async fn handle(
70		&self,
71		request_header: &SataPacketHeader,
72		command_info: &SataCommandInfo,
73		host_filesystem: &HostFilesystem,
74	) -> Result<Bytes, CatBridgeError> {
75		let Ok(final_location) = host_filesystem.resolve_path(&self.path) else {
76			return Self::construct_error(request_header, PATH_NOT_EXIST_ERROR);
77		};
78		let ResolvedLocation::Filesystem(fs_location) = final_location else {
79			todo!("network shares not yet implemented!")
80		};
81
82		// Since you can only have one of 'raw', if we want to check for
83		// 'a' || 'w', we can instead check for the absence of 'r'.
84		if !host_filesystem.path_allows_writes(fs_location.resolved_path())
85			&& (!self.mode_string.contains('r') || self.mode_string.contains('+'))
86		{
87			return Self::construct_error(request_header, FS_ERROR);
88		}
89		let [allow_becoming_write, _, _, _] = command_info.capabilities().1.to_be_bytes();
90		// If it exists, we potentially need to change read only mode flag.
91		if fs_location.resolved_path().exists() {
92			let Ok(metadata) = fs_location.resolved_path().metadata() else {
93				return Self::construct_error(request_header, FS_ERROR);
94			};
95			let mut perms = metadata.permissions();
96			if perms.readonly() && !self.mode_string.contains('r') && allow_becoming_write == 0 {
97				return Self::construct_error(request_header, FS_ERROR);
98			}
99			perms.set_readonly(false);
100			if set_permissions(fs_location.resolved_path(), perms)
101				.await
102				.is_err()
103			{
104				return Self::construct_error(request_header, FS_ERROR);
105			}
106		}
107
108		// Okay time to open!
109		let mut options = OpenOptions::new();
110		if self.mode_string.contains('r') {
111			options.read(true);
112		}
113		if self.mode_string.contains('w') {
114			options.write(true).truncate(true).create(true);
115		}
116		if self.mode_string.contains('a') {
117			options.write(true).truncate(false).create(true);
118		}
119		if self.mode_string.contains('+') {
120			options.create(true);
121		}
122
123		let Ok(fd) = host_filesystem
124			.open_file(options, fs_location.resolved_path())
125			.await
126		else {
127			return Self::construct_error(request_header, FS_ERROR);
128		};
129
130		let mut buff = BytesMut::with_capacity(8);
131		buff.put_u32(0);
132		buff.put_i32(fd);
133		Ok(construct_sata_response(request_header, 0, buff.freeze())?)
134	}
135
136	fn construct_error(
137		packet_header: &SataPacketHeader,
138		error_code: u32,
139	) -> Result<Bytes, CatBridgeError> {
140		let mut buff = BytesMut::with_capacity(8);
141		buff.put_u32(error_code);
142		buff.put_u32(0);
143
144		Ok(construct_sata_response(packet_header, 0, buff.freeze())?)
145	}
146}
147
148impl TryFrom<Bytes> for SataOpenFilePacketBody {
149	type Error = NetworkParseError;
150
151	fn try_from(value: Bytes) -> Result<Self, Self::Error> {
152		if value.len() < 0x210 {
153			return Err(NetworkParseError::FieldNotLongEnough(
154				"SataOpenFile",
155				"Body",
156				0x210,
157				value.len(),
158				value,
159			));
160		}
161		if value.len() > 0x210 {
162			return Err(NetworkParseError::UnexpectedTrailer(
163				"SataOpenFile",
164				value.slice(0x210..),
165			));
166		}
167
168		let (mode_bytes, path_bytes) = value.split_at(0x10);
169		let mode_c_str =
170			CStr::from_bytes_until_nul(mode_bytes).map_err(NetworkParseError::BadCString)?;
171		let path_c_str =
172			CStr::from_bytes_until_nul(path_bytes).map_err(NetworkParseError::BadCString)?;
173		let final_mode = mode_c_str.to_str()?.to_owned();
174		for (idx, car) in final_mode.chars().enumerate() {
175			if idx == 0 && !['r', 'w', 'a'].contains(&car) {
176				return Err(SataProtocolError::BadModeString(final_mode).into());
177			}
178			if idx > 2 {
179				return Err(SataProtocolError::BadModeString(final_mode).into());
180			}
181			if idx != 0 && !['b', '+'].contains(&car) {
182				return Err(SataProtocolError::BadModeString(final_mode).into());
183			}
184		}
185
186		Ok(Self {
187			mode_string: final_mode,
188			path: path_c_str.to_str()?.to_owned(),
189		})
190	}
191}
192
193const SATA_OPEN_FILE_PACKET_BODY_FIELDS: &[NamedField<'static>] = &[NamedField::new("path")];
194
195impl Structable for SataOpenFilePacketBody {
196	fn definition(&self) -> StructDef<'_> {
197		StructDef::new_static(
198			"SataOpenFilePacketBody",
199			Fields::Named(SATA_OPEN_FILE_PACKET_BODY_FIELDS),
200		)
201	}
202}
203
204impl Valuable for SataOpenFilePacketBody {
205	fn as_value(&self) -> Value<'_> {
206		Value::Structable(self)
207	}
208
209	fn visit(&self, visitor: &mut dyn Visit) {
210		visitor.visit_named_fields(&NamedValues::new(
211			SATA_OPEN_FILE_PACKET_BODY_FIELDS,
212			&[Valuable::as_value(&self.path)],
213		));
214	}
215}
216
217#[cfg(test)]
218mod unit_tests {
219	use super::*;
220	use crate::fsemul::host_filesystem::test_helpers::{
221		create_temporary_host_filesystem, join_many,
222	};
223
224	#[tokio::test]
225	pub async fn simple_open_file_request() {
226		let (tempdir, fs) = create_temporary_host_filesystem().await;
227		let request = SataOpenFilePacketBody {
228			path: "/%SLC_EMU_DIR/to-query/file.txt".to_owned(),
229			mode_string: "r".to_owned(),
230		};
231		let mocked_header = SataPacketHeader {
232			packet_data_len: 0,
233			packet_id: 0,
234			flags: 0,
235			version: 0,
236			timestamp_on_host: 0,
237			pid_on_host: 0,
238		};
239		let mocked_command_info = SataCommandInfo {
240			user: (0, 0),
241			capabilities: (0, 0),
242			command: 0x5,
243		};
244
245		let base_dir = join_many(tempdir.path(), ["data", "slc", "to-query"]);
246		tokio::fs::create_dir(&base_dir)
247			.await
248			.expect("Failed to create temporary directory for test!");
249		tokio::fs::write(join_many(&base_dir, ["file.txt"]), vec![0; 1307])
250			.await
251			.expect("Failed to write test file!");
252
253		let mut response = request
254			.handle(&mocked_header, &mocked_command_info, &fs)
255			.await
256			.expect("Failed to handle change mode!");
257		assert_eq!(response.len(), 8 + 0x20, "Packet is not correct size!");
258		// Okay first chop off the header, we don't care.
259		_ = response.split_to(0x20);
260		assert_eq!(
261			&response[..4],
262			&[0x00, 0x00, 0x00, 0x00], // RC
263		);
264		assert_ne!(
265			&response[4..],
266			&[0x00, 0x00, 0x00, 0x00], // File handle.
267		);
268		fs.close_file(i32::from_be_bytes([
269			response[4],
270			response[5],
271			response[6],
272			response[7],
273		]))
274		.await;
275	}
276}