cat-dev 0.0.14

A library for interacting with the CAT-DEV hardware units distributed by Nintendo (i.e. a type of Wii-U DevKits).
Documentation
//! An "open file handle", that is capable of interacting with data that is
//! local, or remotely on NUS.

use crate::{
	errors::{CatBridgeError, FSError},
	fsemul::filesystem::host::utilities::get_new_unique_file_fd,
};
use std::path::{Path, PathBuf};
use tokio::fs::{File, OpenOptions};
use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};

#[cfg(feature = "nus")]
use crate::fsemul::filesystem::nus_fuse::NUSFuse;
#[cfg(feature = "nus")]
use sachet::{common::CafeContentFileInformation, title::TitleID};

#[cfg(any(feature = "clients", feature = "servers"))]
use crate::fsemul::{filesystem::host::HostFilesystem, pcfs::sata::proto::SataFDInfo};

#[derive(Debug)]
/// A 'open' file handle that is capable of acting exactly like a file that
/// anyone would expect, even when the file is remotely on NUS.
pub struct OpenFileHandle {
	/// The path on disk where this file should live. This will either fully exist
	/// when a local file is opened, and ran. Or when dealing with NUS the path to
	/// eventually write too.
	disk_path: PathBuf,
	/// The final size of the file.
	file_size: u64,
	/// The current local file.
	local_file_handle: Option<(File, i32)>,
	#[cfg(feature = "nus")]
	/// The NUS file information and pointer.
	nus_file_info: Option<(TitleID, CafeContentFileInformation, i32)>,
	/// The actual open options we use to open the file.
	open_options: OpenOptions,
	/// The stream which owns this file handle, and should be the only one who can
	/// access this.
	owner: Option<u64>,
}

impl OpenFileHandle {
	/// Attempt to open a file, both locally, and potentially remotely on NUS.
	///
	/// ## Errors
	///
	/// If we cannot open the file from NUS, or locally.
	pub(crate) async fn open_file(
		force_unique_fds: bool,
		open_options: OpenOptions,
		full_disk_path: &Path,
		stream_owner: Option<u64>,
		#[cfg(feature = "nus")] mlc_usr_title_path: &Path,
		#[cfg(feature = "nus")] nus: Option<&NUSFuse>,
	) -> Result<Self, FSError> {
		#[cfg(feature = "nus")]
		if let Some(result) = Self::check_and_potentially_open_from_nus(
			nus,
			mlc_usr_title_path,
			full_disk_path,
			open_options.clone(),
			stream_owner,
		)
		.await
		{
			return Ok(result);
		}

		let local_file = open_options.clone().open(full_disk_path).await?;
		let raw_fd;
		#[cfg(unix)]
		{
			use std::os::fd::AsRawFd;
			raw_fd = local_file.as_raw_fd();
		}
		#[cfg(target_os = "windows")]
		{
			use std::os::windows::io::AsRawHandle;
			raw_fd = local_file.as_raw_handle() as i32;
		}
		#[cfg(all(not(unix), not(target_os = "windows")))]
		{
			raw_fd = get_new_unique_file_fd();
		}
		let md = local_file.metadata().await?;
		let final_fd = if force_unique_fds {
			get_new_unique_file_fd()
		} else {
			raw_fd
		};

		Ok(Self {
			disk_path: full_disk_path.to_path_buf(),
			file_size: md.len(),
			local_file_handle: Some((local_file, final_fd)),
			#[cfg(feature = "nus")]
			nus_file_info: None,
			open_options,
			owner: stream_owner,
		})
	}

	#[must_use]
	pub fn disk_path(&self) -> &Path {
		&self.disk_path
	}

	/// Get the file descriptor number for this particular open file.
	#[must_use]
	pub const fn fd(&self) -> i32 {
		if let Some((_, fd)) = self.local_file_handle.as_ref() {
			return *fd;
		}
		#[cfg(feature = "nus")]
		{
			if let Some((_, _, fd)) = self.nus_file_info.as_ref() {
				return *fd;
			}
		}

		unreachable!();
	}

	/// Get the size of this particular file.
	#[must_use]
	pub const fn file_size(&self) -> u64 {
		self.file_size
	}

	/// Get a file handle that you can actually operate with.
	///
	/// ***CALLING THIS METHOD WILL DOWNLOAD THE FILE FROM NUS. YOU SHOULD BE
	/// ABSOLUTELY POSITIVE YOU NEED THE FILE HANDLE AND CANNOT GET AWAY WITH THE
	/// METADATA AT THIS POINT.***
	///
	/// ## Errors
	///
	/// If the file is on NUS, and we cannot download/decrypt this file/place it
	/// on disk.
	pub async fn force_file_handle(
		&mut self,
		#[cfg(feature = "nus")] nus: Option<&NUSFuse>,
	) -> Result<&mut File, CatBridgeError> {
		#[cfg(feature = "nus")]
		if let Some((title_id, file_info, fd)) = self.nus_file_info {
			let nc = nus.ok_or_else(|| {
				FSError::IO(std::io::Error::other(
					"NUS file pointer without NUS client?",
				))
			})?;
			nc.download_to(&self.disk_path, title_id, file_info).await?;
			self.nus_file_info = None;
			_ = self.local_file_handle.insert((
				self.open_options
					.open(&self.disk_path)
					.await
					.map_err(FSError::IO)?,
				fd,
			));
		}

		if let Some((handle, _)) = self.local_file_handle.as_mut() {
			return Ok(handle);
		}

		unreachable!();
	}

	#[cfg_attr(docsrs, doc(cfg(any(feature = "clients", feature = "servers"))))]
	#[cfg(any(feature = "clients", feature = "servers"))]
	/// Get the FD info for this particular open file handle.
	///
	/// ## Errors
	///
	/// If we cannot get the metadata for the open local file handle.
	pub async fn sata_fd_info(
		&self,
		host_filesystem: &HostFilesystem,
	) -> Result<SataFDInfo, FSError> {
		#[cfg(feature = "nus")]
		{
			if let Some((_title_id, file_info, _fd)) = self.nus_file_info.as_ref() {
				return Ok(SataFDInfo::new_from_nus(
					host_filesystem,
					&self.disk_path,
					Some(*file_info),
				)
				.await);
			}
		}

		if let Some((handle, _)) = self.local_file_handle.as_ref() {
			return Ok(SataFDInfo::get_info(
				host_filesystem,
				&handle.metadata().await?,
				&self.disk_path,
			)
			.await);
		}

		unreachable!()
	}

	/// Get the stream that owns this particular file handle.
	#[must_use]
	pub const fn stream_owner(&self) -> Option<u64> {
		self.owner
	}

	#[cfg(feature = "nus")]
	/// Check if a file should be loaded from NUS, and load it from NUS.
	#[must_use]
	async fn check_and_potentially_open_from_nus(
		opt_nus: Option<&NUSFuse>,
		mlc_user_title_path: &Path,
		full_disk_path: &Path,
		open_options: OpenOptions,
		stream_owner: Option<u64>,
	) -> Option<Self> {
		let nus = opt_nus?;
		if full_disk_path.exists() {
			return None;
		}
		let relative_path = full_disk_path.strip_prefix(mlc_user_title_path).ok()?;
		if relative_path.components().count() < 3 {
			return None;
		}
		let mut comp = relative_path.components();
		let group =
			u32::from_str_radix(comp.next()?.as_os_str().to_string_lossy().as_ref(), 16).ok()?;
		let title =
			u32::from_str_radix(comp.next()?.as_os_str().to_string_lossy().as_ref(), 16).ok()?;
		let tid = TitleID::new_with_ids(group, title);
		let relative_path = comp.as_path();
		let nus_info = nus.exists(tid, relative_path).await??;
		let unique_fd = get_new_unique_file_fd();

		Some(Self {
			disk_path: full_disk_path.to_path_buf(),
			file_size: u64::from(nus_info.file_size()),
			local_file_handle: None,
			nus_file_info: Some((tid, nus_info, unique_fd)),
			open_options,
			owner: stream_owner,
		})
	}
}

const OPEN_FILE_HANDLE_FIELDS: &[NamedField<'static>] = &[
	NamedField::new("disk_path"),
	NamedField::new("file_size"),
	NamedField::new("local_file_handle"),
	#[cfg(feature = "nus")]
	NamedField::new("nus_file_info"),
	NamedField::new("open_options"),
];

impl Structable for OpenFileHandle {
	fn definition(&self) -> StructDef<'_> {
		StructDef::new_static("OpenFileHandle", Fields::Named(OPEN_FILE_HANDLE_FIELDS))
	}
}

impl Valuable for OpenFileHandle {
	fn as_value(&self) -> Value<'_> {
		Value::Structable(self)
	}

	fn visit(&self, visitor: &mut dyn Visit) {
		visitor.visit_named_fields(&NamedValues::new(
			OPEN_FILE_HANDLE_FIELDS,
			&[
				Valuable::as_value(&self.disk_path),
				Valuable::as_value(&self.file_size),
				Valuable::as_value(&format!("{:?}", self.local_file_handle)),
				#[cfg(feature = "nus")]
				Valuable::as_value(&self.nus_file_info),
				Valuable::as_value(&format!("{:?}", self.open_options)),
			],
		));
	}
}