Skip to main content

cat_dev/fsemul/filesystem/host/
open_file.rs

1//! An "open file handle", that is capable of interacting with data that is
2//! local, or remotely on NUS.
3
4use crate::{
5	errors::{CatBridgeError, FSError},
6	fsemul::filesystem::host::utilities::get_new_unique_file_fd,
7};
8use std::path::{Path, PathBuf};
9use tokio::fs::{File, OpenOptions};
10use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
11
12#[cfg(feature = "nus")]
13use crate::fsemul::filesystem::nus_fuse::NUSFuse;
14#[cfg(feature = "nus")]
15use sachet::{common::CafeContentFileInformation, title::TitleID};
16
17#[cfg(any(feature = "clients", feature = "servers"))]
18use crate::fsemul::{filesystem::host::HostFilesystem, pcfs::sata::proto::SataFDInfo};
19
20#[derive(Debug)]
21/// A 'open' file handle that is capable of acting exactly like a file that
22/// anyone would expect, even when the file is remotely on NUS.
23pub struct OpenFileHandle {
24	/// The path on disk where this file should live. This will either fully exist
25	/// when a local file is opened, and ran. Or when dealing with NUS the path to
26	/// eventually write too.
27	disk_path: PathBuf,
28	/// The final size of the file.
29	file_size: u64,
30	/// The current local file.
31	local_file_handle: Option<(File, i32)>,
32	#[cfg(feature = "nus")]
33	/// The NUS file information and pointer.
34	nus_file_info: Option<(TitleID, CafeContentFileInformation, i32)>,
35	/// The actual open options we use to open the file.
36	open_options: OpenOptions,
37	/// The stream which owns this file handle, and should be the only one who can
38	/// access this.
39	owner: Option<u64>,
40}
41
42impl OpenFileHandle {
43	/// Attempt to open a file, both locally, and potentially remotely on NUS.
44	///
45	/// ## Errors
46	///
47	/// If we cannot open the file from NUS, or locally.
48	pub(crate) async fn open_file(
49		force_unique_fds: bool,
50		open_options: OpenOptions,
51		full_disk_path: &Path,
52		stream_owner: Option<u64>,
53		#[cfg(feature = "nus")] mlc_usr_title_path: &Path,
54		#[cfg(feature = "nus")] nus: Option<&NUSFuse>,
55	) -> Result<Self, FSError> {
56		#[cfg(feature = "nus")]
57		if let Some(result) = Self::check_and_potentially_open_from_nus(
58			nus,
59			mlc_usr_title_path,
60			full_disk_path,
61			open_options.clone(),
62			stream_owner,
63		)
64		.await
65		{
66			return Ok(result);
67		}
68
69		let local_file = open_options.clone().open(full_disk_path).await?;
70		let raw_fd;
71		#[cfg(unix)]
72		{
73			use std::os::fd::AsRawFd;
74			raw_fd = local_file.as_raw_fd();
75		}
76		#[cfg(target_os = "windows")]
77		{
78			use std::os::windows::io::AsRawHandle;
79			raw_fd = local_file.as_raw_handle() as i32;
80		}
81		#[cfg(all(not(unix), not(target_os = "windows")))]
82		{
83			raw_fd = get_new_unique_file_fd();
84		}
85		let md = local_file.metadata().await?;
86		let final_fd = if force_unique_fds {
87			get_new_unique_file_fd()
88		} else {
89			raw_fd
90		};
91
92		Ok(Self {
93			disk_path: full_disk_path.to_path_buf(),
94			file_size: md.len(),
95			local_file_handle: Some((local_file, final_fd)),
96			#[cfg(feature = "nus")]
97			nus_file_info: None,
98			open_options,
99			owner: stream_owner,
100		})
101	}
102
103	#[must_use]
104	pub fn disk_path(&self) -> &Path {
105		&self.disk_path
106	}
107
108	/// Get the file descriptor number for this particular open file.
109	#[must_use]
110	pub const fn fd(&self) -> i32 {
111		if let Some((_, fd)) = self.local_file_handle.as_ref() {
112			return *fd;
113		}
114		#[cfg(feature = "nus")]
115		{
116			if let Some((_, _, fd)) = self.nus_file_info.as_ref() {
117				return *fd;
118			}
119		}
120
121		unreachable!();
122	}
123
124	/// Get the size of this particular file.
125	#[must_use]
126	pub const fn file_size(&self) -> u64 {
127		self.file_size
128	}
129
130	/// Get a file handle that you can actually operate with.
131	///
132	/// ***CALLING THIS METHOD WILL DOWNLOAD THE FILE FROM NUS. YOU SHOULD BE
133	/// ABSOLUTELY POSITIVE YOU NEED THE FILE HANDLE AND CANNOT GET AWAY WITH THE
134	/// METADATA AT THIS POINT.***
135	///
136	/// ## Errors
137	///
138	/// If the file is on NUS, and we cannot download/decrypt this file/place it
139	/// on disk.
140	pub async fn force_file_handle(
141		&mut self,
142		#[cfg(feature = "nus")] nus: Option<&NUSFuse>,
143	) -> Result<&mut File, CatBridgeError> {
144		#[cfg(feature = "nus")]
145		if let Some((title_id, file_info, fd)) = self.nus_file_info {
146			let nc = nus.ok_or_else(|| {
147				FSError::IO(std::io::Error::other(
148					"NUS file pointer without NUS client?",
149				))
150			})?;
151			nc.download_to(&self.disk_path, title_id, file_info).await?;
152			self.nus_file_info = None;
153			_ = self.local_file_handle.insert((
154				self.open_options
155					.open(&self.disk_path)
156					.await
157					.map_err(FSError::IO)?,
158				fd,
159			));
160		}
161
162		if let Some((handle, _)) = self.local_file_handle.as_mut() {
163			return Ok(handle);
164		}
165
166		unreachable!();
167	}
168
169	#[cfg_attr(docsrs, doc(cfg(any(feature = "clients", feature = "servers"))))]
170	#[cfg(any(feature = "clients", feature = "servers"))]
171	/// Get the FD info for this particular open file handle.
172	///
173	/// ## Errors
174	///
175	/// If we cannot get the metadata for the open local file handle.
176	pub async fn sata_fd_info(
177		&self,
178		host_filesystem: &HostFilesystem,
179	) -> Result<SataFDInfo, FSError> {
180		#[cfg(feature = "nus")]
181		{
182			if let Some((_title_id, file_info, _fd)) = self.nus_file_info.as_ref() {
183				return Ok(SataFDInfo::new_from_nus(
184					host_filesystem,
185					&self.disk_path,
186					Some(*file_info),
187				)
188				.await);
189			}
190		}
191
192		if let Some((handle, _)) = self.local_file_handle.as_ref() {
193			return Ok(SataFDInfo::get_info(
194				host_filesystem,
195				&handle.metadata().await?,
196				&self.disk_path,
197			)
198			.await);
199		}
200
201		unreachable!()
202	}
203
204	/// Get the stream that owns this particular file handle.
205	#[must_use]
206	pub const fn stream_owner(&self) -> Option<u64> {
207		self.owner
208	}
209
210	#[cfg(feature = "nus")]
211	/// Check if a file should be loaded from NUS, and load it from NUS.
212	#[must_use]
213	async fn check_and_potentially_open_from_nus(
214		opt_nus: Option<&NUSFuse>,
215		mlc_user_title_path: &Path,
216		full_disk_path: &Path,
217		open_options: OpenOptions,
218		stream_owner: Option<u64>,
219	) -> Option<Self> {
220		let nus = opt_nus?;
221		if full_disk_path.exists() {
222			return None;
223		}
224		let relative_path = full_disk_path.strip_prefix(mlc_user_title_path).ok()?;
225		if relative_path.components().count() < 3 {
226			return None;
227		}
228		let mut comp = relative_path.components();
229		let group =
230			u32::from_str_radix(comp.next()?.as_os_str().to_string_lossy().as_ref(), 16).ok()?;
231		let title =
232			u32::from_str_radix(comp.next()?.as_os_str().to_string_lossy().as_ref(), 16).ok()?;
233		let tid = TitleID::new_with_ids(group, title);
234		let relative_path = comp.as_path();
235		let nus_info = nus.exists(tid, relative_path).await??;
236		let unique_fd = get_new_unique_file_fd();
237
238		Some(Self {
239			disk_path: full_disk_path.to_path_buf(),
240			file_size: u64::from(nus_info.file_size()),
241			local_file_handle: None,
242			nus_file_info: Some((tid, nus_info, unique_fd)),
243			open_options,
244			owner: stream_owner,
245		})
246	}
247}
248
249const OPEN_FILE_HANDLE_FIELDS: &[NamedField<'static>] = &[
250	NamedField::new("disk_path"),
251	NamedField::new("file_size"),
252	NamedField::new("local_file_handle"),
253	#[cfg(feature = "nus")]
254	NamedField::new("nus_file_info"),
255	NamedField::new("open_options"),
256];
257
258impl Structable for OpenFileHandle {
259	fn definition(&self) -> StructDef<'_> {
260		StructDef::new_static("OpenFileHandle", Fields::Named(OPEN_FILE_HANDLE_FIELDS))
261	}
262}
263
264impl Valuable for OpenFileHandle {
265	fn as_value(&self) -> Value<'_> {
266		Value::Structable(self)
267	}
268
269	fn visit(&self, visitor: &mut dyn Visit) {
270		visitor.visit_named_fields(&NamedValues::new(
271			OPEN_FILE_HANDLE_FIELDS,
272			&[
273				Valuable::as_value(&self.disk_path),
274				Valuable::as_value(&self.file_size),
275				Valuable::as_value(&format!("{:?}", self.local_file_handle)),
276				#[cfg(feature = "nus")]
277				Valuable::as_value(&self.nus_file_info),
278				Valuable::as_value(&format!("{:?}", self.open_options)),
279			],
280		));
281	}
282}