cat_dev/fsemul/pcfs/sata_proto/
open_folder.rs

1//! Definitions, and handlers for the `OpenFolder` packet type.
2//!
3//! This does not iterate over the directory at all, just opens the directory.
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 valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
16
17/// A filesystem error occured.
18const FS_ERROR: u32 = 0xFFF0_FFE0;
19/// An error code to send when a path does not exist.
20///
21/// This is also used in some places that are a bit of a stretch like for
22/// network shares on disk space. The path doesn't exist on a disk, so this
23/// error code is used, even if it's not quite exact.
24const PATH_NOT_EXIST_ERROR: u32 = 0xFFF0_FFE9;
25
26/// A packet to open an iterator over a folders contents.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct SataOpenFolderPacketBody {
29	/// The path to query, note that this is not the 'resolved' path which is
30	/// the path to actual read from.
31	///
32	/// Interpolation has a few known ways of being replaced:
33	///
34	/// - `%MLC_EMU_DIR`: `<cafe_sdk>/data/mlc/`
35	/// - `%SLC_EMU_DIR`: `<cafe_sdk>/data/slc/`
36	/// - `%DISC_EMU_DIR`: `<cafe_sdk>/data/disc/`
37	/// - `%SAVE_EMU_DIR`: `<cafe_sdk>/data/save/`
38	/// - `%NETWORK`: <mounted network share path>
39	path: String,
40}
41
42impl SataOpenFolderPacketBody {
43	#[must_use]
44	pub fn path(&self) -> &str {
45		self.path.as_str()
46	}
47
48	/// Handle opening a folder upon request.
49	///
50	/// ## Errors
51	///
52	/// If we cannot construct a sata response packet because our data to send
53	/// was somehow too large (this should ideally never happen).
54	pub async fn handle(
55		&self,
56		request_header: &SataPacketHeader,
57		host_filesystem: &HostFilesystem,
58	) -> Result<Bytes, CatBridgeError> {
59		let Ok(final_location) = host_filesystem.resolve_path(&self.path) else {
60			return Self::construct_error(request_header, PATH_NOT_EXIST_ERROR);
61		};
62		let ResolvedLocation::Filesystem(fs_location) = final_location else {
63			todo!("network shares not yet implemented!")
64		};
65
66		let Ok(fd) = host_filesystem
67			.open_folder(fs_location.resolved_path())
68			.await
69		else {
70			return Self::construct_error(request_header, FS_ERROR);
71		};
72
73		let mut buff = BytesMut::with_capacity(8);
74		buff.put_u32(0);
75		buff.put_i32(fd);
76		Ok(construct_sata_response(request_header, 0, buff.freeze())?)
77	}
78
79	fn construct_error(
80		packet_header: &SataPacketHeader,
81		error_code: u32,
82	) -> Result<Bytes, CatBridgeError> {
83		let mut buff = BytesMut::with_capacity(8);
84		buff.put_u32(error_code);
85		buff.put_u32(0);
86
87		Ok(construct_sata_response(packet_header, 0, buff.freeze())?)
88	}
89}
90
91impl TryFrom<Bytes> for SataOpenFolderPacketBody {
92	type Error = NetworkParseError;
93
94	fn try_from(value: Bytes) -> Result<Self, Self::Error> {
95		if value.len() < 0x200 {
96			return Err(NetworkParseError::FieldNotLongEnough(
97				"SataOpenFolder",
98				"Body",
99				0x200,
100				value.len(),
101				value,
102			));
103		}
104		if value.len() > 0x200 {
105			return Err(NetworkParseError::UnexpectedTrailer(
106				"SataOpenFolder",
107				value.slice(0x200..),
108			));
109		}
110
111		let path_c_str =
112			CStr::from_bytes_until_nul(&value).map_err(NetworkParseError::BadCString)?;
113
114		Ok(Self {
115			path: path_c_str.to_str()?.to_owned(),
116		})
117	}
118}
119
120const SATA_OPEN_FOLDER_PACKET_BODY_FIELDS: &[NamedField<'static>] = &[NamedField::new("path")];
121
122impl Structable for SataOpenFolderPacketBody {
123	fn definition(&self) -> StructDef<'_> {
124		StructDef::new_static(
125			"SataOpenFolderPacketBody",
126			Fields::Named(SATA_OPEN_FOLDER_PACKET_BODY_FIELDS),
127		)
128	}
129}
130
131impl Valuable for SataOpenFolderPacketBody {
132	fn as_value(&self) -> Value<'_> {
133		Value::Structable(self)
134	}
135
136	fn visit(&self, visitor: &mut dyn Visit) {
137		visitor.visit_named_fields(&NamedValues::new(
138			SATA_OPEN_FOLDER_PACKET_BODY_FIELDS,
139			&[Valuable::as_value(&self.path)],
140		));
141	}
142}
143
144#[cfg(test)]
145mod unit_tests {
146	use super::*;
147	use crate::fsemul::host_filesystem::test_helpers::{
148		create_temporary_host_filesystem, join_many,
149	};
150
151	#[tokio::test]
152	pub async fn simple_open_folder_request() {
153		let (tempdir, fs) = create_temporary_host_filesystem().await;
154		let request = SataOpenFolderPacketBody {
155			path: "/%SLC_EMU_DIR/to-query/".to_owned(),
156		};
157		let mocked_header = SataPacketHeader {
158			packet_data_len: 0,
159			packet_id: 0,
160			flags: 0,
161			version: 0,
162			timestamp_on_host: 0,
163			pid_on_host: 0,
164		};
165
166		let base_dir = join_many(tempdir.path(), ["data", "slc", "to-query"]);
167		tokio::fs::create_dir(&base_dir)
168			.await
169			.expect("Failed to create temporary directory for test!");
170
171		let mut response = request
172			.handle(&mocked_header, &fs)
173			.await
174			.expect("Failed to handle change mode!");
175		assert_eq!(response.len(), 8 + 0x20, "Packet is not correct size!");
176		// Okay first chop off the header, we don't care.
177		_ = response.split_to(0x20);
178		assert_eq!(
179			&response[..4],
180			&[0x00, 0x00, 0x00, 0x00], // RC
181		);
182		assert_ne!(
183			&response[4..],
184			&[0x00, 0x00, 0x00, 0x00], // folder handle.
185		);
186		fs.close_folder(i32::from_be_bytes([
187			response[4],
188			response[5],
189			response[6],
190			response[7],
191		]))
192		.await;
193	}
194}