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
//! Utilities for managing 'Open Folders'.

use crate::{errors::FSError, fsemul::filesystem::ItemInFolder};
use std::path::{Path, PathBuf};
use tokio::fs::read_dir;
use valuable::Valuable;

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

/// An open 'directory listing'.
#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
pub struct DirectoryListing {
	/// The files that are local and exist on the actual filesystem.
	local_files: Vec<PathBuf>,
	#[cfg(feature = "nus")]
	/// The files that don't exist locally, and are only in NUS.
	nus_files: Vec<(
		Option<TitleID>,
		PathBuf,
		PathBuf,
		Option<CafeContentFileInformation>,
	)>,
	/// The current 'index' this is guaranteed to always be less than
	/// `self.local_files + self.nus_files`, and transparently switches
	/// between the two.
	current_idx: usize,
	/// A helper to know that we're 'at the end' without needing to recheck.
	is_at_end: bool,
	/// The root path this directory listing is at.
	root_path: PathBuf,
	/// The owner of this particular stream.
	stream_owner: Option<u64>,
}

impl DirectoryListing {
	/// List all the files within a directory, locally, and remotely.
	pub async fn new(
		path: &Path,
		mlc_dir: PathBuf,
		for_stream: Option<u64>,
		#[cfg(feature = "nus")] nus_client: Option<&NUSFuse>,
	) -> Result<Self, FSError> {
		let mut local_files = Vec::new();
		let mut prev_err: Option<FSError> = None;
		match read_dir(path).await {
			Ok(mut stream) => {
				while let Ok(Some(item)) = stream.next_entry().await {
					local_files.push(item.path());
				}
			}
			Err(cause) => {
				prev_err = Some(cause.into());
			}
		}
		local_files.sort();

		#[cfg(feature = "nus")]
		let nus_files = Self::get_items_from_nus(&local_files, path, mlc_dir, nus_client).await?;
		#[cfg(feature = "nus")]
		if let Some(c) = prev_err
			&& nus_files.is_empty()
		{
			return Err(c);
		}

		Ok(Self {
			local_files,
			#[cfg(feature = "nus")]
			nus_files,
			current_idx: 0,
			is_at_end: false,
			root_path: path.to_path_buf(),
			stream_owner: for_stream,
		})
	}

	/// Get the next item in this folder if one exists.
	#[allow(
		// dependent on features.
		unreachable_code,
	)]
	pub fn next_item(&mut self) -> Option<ItemInFolder> {
		if self.is_at_end {
			return None;
		}

		let max_files: usize;
		#[cfg(feature = "nus")]
		{
			max_files = self.local_files.len() + self.nus_files.len();
		}
		#[cfg(not(feature = "nus"))]
		{
			max_files = self.local_files.len();
		}
		if self.current_idx >= max_files {
			self.is_at_end = true;
			return None;
		}

		while self.current_idx < self.local_files.len() {
			let next_item = &self.local_files[self.current_idx];
			self.current_idx += 1;

			if (!next_item.is_file() && !next_item.is_dir()) || next_item.is_symlink() {
				continue;
			}

			return Some(ItemInFolder::Local(
				next_item.clone(),
				self.root_path.components().count(),
			));
		}
		#[cfg(feature = "nus")]
		{
			let actual_idx = self.current_idx - self.local_files.len();
			self.current_idx += 1;
			let nus_item = &self.nus_files[actual_idx];

			return Some(ItemInFolder::Nus(
				nus_item.0,
				nus_item.1.clone(),
				nus_item.2.clone(),
				nus_item.3,
			));
		}

		unreachable!("Not reachable path, will either return from NUS.")
	}

	pub const fn reverse_folder(&mut self) {
		if self.current_idx == 0 {
			return;
		}

		self.current_idx -= 1;
		self.is_at_end = false;
	}

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

	#[cfg(feature = "nus")]
	/// Get the files that should be in this folder from NUS that don't already exist.
	async fn get_items_from_nus(
		local_files: &[PathBuf],
		path: &Path,
		mut mlc_dir: PathBuf,
		nus_client: Option<&NUSFuse>,
	) -> Result<
		Vec<(
			Option<TitleID>,
			PathBuf,
			PathBuf,
			Option<CafeContentFileInformation>,
		)>,
		FSError,
	> {
		let Some(nc) = nus_client else {
			return Ok(Vec::with_capacity(0));
		};

		mlc_dir.push("usr");
		mlc_dir.push("title");
		let Ok(leftover) = path.strip_prefix(&mlc_dir) else {
			return Ok(Vec::with_capacity(0));
		};

		let mut nus_files = Vec::new();
		if leftover.components().count() == 0 {
			let gids = nc.get_sorted_group_ids();
			for group in gids {
				let mut final_path = mlc_dir.clone();
				final_path.push("{group:08x}");
				if !local_files.contains(&final_path) {
					nus_files.push((
						None,
						PathBuf::from(format!("{group:08x}")),
						final_path,
						None,
					));
				}
			}
		} else if leftover.components().count() == 1 {
			let Some(gid_component) = leftover.components().next() else {
				return Ok(Vec::with_capacity(0));
			};
			let Ok(gid) =
				u32::from_str_radix(gid_component.as_os_str().to_string_lossy().as_ref(), 16)
			else {
				return Ok(Vec::with_capacity(0));
			};
			let tids = nc.get_sorted_tids_in_group(gid);

			let mut base_path = mlc_dir;
			base_path.push(format!("{gid:08x}"));
			for title in tids {
				let mut final_path = base_path.clone();
				final_path.push("{title:08x}");
				if !local_files.contains(&final_path) {
					nus_files.push((
						None,
						PathBuf::from(format!("{title:08x}")),
						final_path,
						None,
					));
				}
			}
		} else {
			let mut components = leftover.components();
			let Some(gid_component) = components.next() else {
				return Ok(Vec::with_capacity(0));
			};
			let Ok(group) =
				u32::from_str_radix(gid_component.as_os_str().to_string_lossy().as_ref(), 16)
			else {
				return Ok(Vec::with_capacity(0));
			};
			let Some(tid_component) = components.next() else {
				return Ok(Vec::with_capacity(0));
			};
			let Ok(title) =
				u32::from_str_radix(tid_component.as_os_str().to_string_lossy().as_ref(), 16)
			else {
				return Ok(Vec::with_capacity(0));
			};
			let title_id = TitleID::new_with_ids(group, title);

			let mut final_path = mlc_dir;
			final_path.push(format!("{group:08x}"));
			final_path.push(format!("{title:08x}"));

			let mut seen_one = false;
			let mut is_code = false;
			for comp in leftover.components().skip(2) {
				is_code = !seen_one && comp.as_os_str().to_string_lossy().as_ref() == "code";
				final_path.push(comp);
				seen_one = true;
			}
			for item in nc
				.get_files_in_folder(title_id, components.as_path(), is_code)
				.await
			{
				let mut final_path_frd = final_path.clone();
				for component_part in item.0.components() {
					final_path_frd.push(component_part);
				}
				nus_files.push((Some(title_id), item.0, final_path_frd, item.1));
			}
		}

		Ok(nus_files)
	}
}