Skip to main content

cat_dev/fsemul/pcfs/sata/proto/
get_info_by_query.rs

1//! Definitions for the `GetQueryByInfo` packet type, and it's response types.
2//!
3//! Although this name implies there's some sort of querying, or something
4//! going on there's actually not. You simply give us a file path, and we
5//! give you either file information, the count of files in a directory, or
6//! the size of a particular file. Wow.
7
8use crate::{
9	errors::NetworkParseError,
10	fsemul::{
11		filesystem::host::HostFilesystem,
12		pcfs::errors::{PcfsApiError, SataProtocolError},
13	},
14};
15use bytes::{Buf, BufMut, Bytes, BytesMut};
16use std::{
17	ffi::CStr,
18	fs::Metadata,
19	path::PathBuf,
20	sync::LazyLock,
21	time::{Duration, SystemTime},
22};
23use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
24
25#[cfg(feature = "nus")]
26use sachet::common::CafeContentFileInformation;
27
28/// Timestamps are "FAT" timestamps which start in 1980.
29static FAT_TIMESTAMP_START: LazyLock<SystemTime> = LazyLock::new(|| {
30	SystemTime::UNIX_EPOCH
31		.checked_add(Duration::from_hours(87650))
32		.expect("Failed to get timestamp for 1980! required!")
33});
34
35/// A packet to get information about a particular directory path.
36///
37/// This can do everything from "get the free space of the disk this path
38/// is on", to "get my some metadata about this very specific path."
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct SataGetInfoByQueryPacketBody {
41	/// The path to query, note that this is not the 'resolved' path which is
42	/// the path to actual read from.
43	///
44	/// Interpolation has a few known ways of being replaced:
45	///
46	/// - `%MLC_EMU_DIR`: `<cafe_sdk>/data/mlc/`
47	/// - `%SLC_EMU_DIR`: `<cafe_sdk>/data/slc/`
48	/// - `%DISC_EMU_DIR`: `<cafe_sdk>/data/disc/`
49	/// - `%SAVE_EMU_DIR`: `<cafe_sdk>/data/save/`
50	/// - `%NETWORK`: <mounted network share path>
51	path: String,
52	/// The type of information we're looking for.
53	typ: SataQueryType,
54}
55
56impl SataGetInfoByQueryPacketBody {
57	/// Attempt to construct a new packet to get info about some path.
58	///
59	/// ## Errors
60	///
61	/// If the path is longer than 511 bytes. Normally the max path is 512 bytes,
62	/// but because we need to encode our data as a C-String with a NUL
63	/// terminator we cannot be longer than 511 bytes.
64	///
65	/// Consider using relative/mapped paths if possible when dealing with long
66	/// paths.
67	pub fn new(path: String, query_type: SataQueryType) -> Result<Self, PcfsApiError> {
68		if path.len() > 511 {
69			return Err(PcfsApiError::PathTooLong(path));
70		}
71
72		Ok(Self {
73			path,
74			typ: query_type,
75		})
76	}
77
78	#[must_use]
79	pub const fn query_type(&self) -> SataQueryType {
80		self.typ
81	}
82
83	pub const fn set_query_type(&mut self, new_type: SataQueryType) {
84		self.typ = new_type;
85	}
86
87	#[must_use]
88	pub fn path(&self) -> &str {
89		self.path.as_str()
90	}
91
92	/// Update the path to send in this particular get info by query packet.
93	///
94	/// ## Errors
95	///
96	/// If the path is longer than 511 bytes. Normally the max path is 512 bytes,
97	/// but because we need to encode our data as a C-String with a NUL
98	/// terminator we cannot be longer than 511 bytes.
99	///
100	/// Consider using relative/mapped paths if possible when dealing with long
101	/// paths.
102	pub fn set_path(&mut self, new_path: String) -> Result<(), PcfsApiError> {
103		if new_path.len() > 511 {
104			return Err(PcfsApiError::PathTooLong(new_path));
105		}
106
107		self.path = new_path;
108		Ok(())
109	}
110}
111
112impl From<&SataGetInfoByQueryPacketBody> for Bytes {
113	fn from(value: &SataGetInfoByQueryPacketBody) -> Self {
114		let mut result = BytesMut::with_capacity(0x204);
115		result.extend_from_slice(value.path.as_bytes());
116		// These are C Strings so we need a NUL terminator.
117		// Pad with `0`, til we get a full path with a nul terminator.
118		result.extend(BytesMut::zeroed(0x200 - result.len()));
119		result.put_u32(u32::from(value.typ));
120		result.freeze()
121	}
122}
123
124impl From<SataGetInfoByQueryPacketBody> for Bytes {
125	fn from(value: SataGetInfoByQueryPacketBody) -> Self {
126		Self::from(&value)
127	}
128}
129
130impl TryFrom<Bytes> for SataGetInfoByQueryPacketBody {
131	type Error = NetworkParseError;
132
133	fn try_from(value: Bytes) -> Result<Self, Self::Error> {
134		if value.len() < 0x204 {
135			return Err(NetworkParseError::FieldNotLongEnough(
136				"SataGetInfoByQuery",
137				"Body",
138				0x204,
139				value.len(),
140				value,
141			));
142		}
143		if value.len() > 0x204 {
144			return Err(NetworkParseError::UnexpectedTrailer(
145				"SataGetInfoByQueryBody",
146				value.slice(0x204..),
147			));
148		}
149
150		let (path_bytes, num) = value.split_at(0x200);
151		let path_c_str =
152			CStr::from_bytes_until_nul(path_bytes).map_err(NetworkParseError::BadCString)?;
153		let query_type = u32::from_be_bytes([num[0], num[1], num[2], num[3]]);
154		let final_path = path_c_str.to_str()?.to_owned();
155
156		Ok(Self {
157			path: final_path,
158			typ: SataQueryType::try_from(query_type)?,
159		})
160	}
161}
162
163const SATA_GET_INFO_BY_QUERY_PACKET_BODY_FIELDS: &[NamedField<'static>] =
164	&[NamedField::new("path"), NamedField::new("type")];
165
166impl Structable for SataGetInfoByQueryPacketBody {
167	fn definition(&self) -> StructDef<'_> {
168		StructDef::new_static(
169			"SataGetInfoByQueryPacketBody",
170			Fields::Named(SATA_GET_INFO_BY_QUERY_PACKET_BODY_FIELDS),
171		)
172	}
173}
174
175impl Valuable for SataGetInfoByQueryPacketBody {
176	fn as_value(&self) -> Value<'_> {
177		Value::Structable(self)
178	}
179
180	fn visit(&self, visitor: &mut dyn Visit) {
181		visitor.visit_named_fields(&NamedValues::new(
182			SATA_GET_INFO_BY_QUERY_PACKET_BODY_FIELDS,
183			&[
184				Valuable::as_value(&self.path),
185				Valuable::as_value(&self.typ),
186			],
187		));
188	}
189}
190
191/// The type of information we're looking for from our request.
192#[derive(Copy, Clone, Debug, PartialEq, Eq, Valuable)]
193pub enum SataQueryType {
194	/// Get the amount of free disk space available to the calling application.
195	FreeDiskSpace,
196	/// Get the size of files in a directory, recursively.
197	SizeOfFolder,
198	/// Get the number of files in a directory, recursively.
199	FileCount,
200	/// Get the information around a particular file or folder.
201	FileDetails,
202}
203
204impl From<SataQueryType> for u32 {
205	fn from(value: SataQueryType) -> Self {
206		match value {
207			SataQueryType::FreeDiskSpace => 0,
208			SataQueryType::SizeOfFolder => 1,
209			SataQueryType::FileCount => 2,
210			SataQueryType::FileDetails => 5,
211		}
212	}
213}
214
215impl TryFrom<u32> for SataQueryType {
216	type Error = SataProtocolError;
217
218	fn try_from(value: u32) -> Result<Self, Self::Error> {
219		match value {
220			0 => Ok(Self::FreeDiskSpace),
221			1 => Ok(Self::SizeOfFolder),
222			2 => Ok(Self::FileCount),
223			5 => Ok(Self::FileDetails),
224			val => Err(SataProtocolError::UnknownGetInfoQueryType(val)),
225		}
226	}
227}
228
229/// A response that came from a particular query response.
230///
231/// These depends on the query type that was actively passed in. Most paths
232/// just return a very basic "size" (e.g. file count, or file length, etc.).
233/// However the file stat query type actually returns all the information about
234/// a particular path.
235#[derive(Clone, Debug, Valuable, PartialEq, Eq)]
236pub enum SataQueryResponse {
237	/// An error has occured, and we are returning an error code.
238	ErrorCode(u32),
239	/// A size response that is guaranteed to fit within a u32.
240	///
241	/// Query types that return this:
242	///
243	/// - [`SataQueryType::FileCount`]
244	SmallSize(u32),
245	/// A size response that is guaranteed to fit within a u64.
246	///
247	/// Query types that return this:
248	///
249	/// - [`SataQueryType::SizeOfFolder`]
250	/// - [`SataQueryType::FreeDiskSpace`]
251	LargeSize(u64),
252	/// All the info about a particular file path.
253	///
254	/// Query types that return this:
255	///
256	/// - [`SataQueryType::FileDetails`]
257	FDInfo(SataFDInfo),
258}
259
260impl SataQueryResponse {
261	/// Try to read a [`SataQueryResponse::SmallSize`] from a full response
262	/// body.
263	///
264	/// ## Errors
265	///
266	/// If the response had an error code, or we could not get all of the [`u32`]
267	/// value of the body.
268	pub fn try_from_small(mut value: Bytes) -> Result<Self, NetworkParseError> {
269		let rc = value.get_u32();
270		if rc != 0 {
271			return Err(NetworkParseError::ErrorCode(rc));
272		}
273
274		let smol = value.get_u32();
275
276		Ok(Self::SmallSize(smol))
277	}
278
279	/// Try to read a [`SataQueryResponse::LargeSize`] from a full response
280	/// body.
281	///
282	/// ## Errors
283	///
284	/// If the response had an error code, or we could not get all of the [`u64`]
285	/// value of the body.
286	pub fn try_from_large(mut value: Bytes) -> Result<Self, NetworkParseError> {
287		let rc = value.get_u32();
288		if rc != 0 {
289			return Err(NetworkParseError::ErrorCode(rc));
290		}
291
292		let larg = value.get_u64();
293
294		Ok(Self::LargeSize(larg))
295	}
296
297	/// Try to read a [`SataQueryResponse::FDInfo`] from a full response
298	/// body.
299	///
300	/// ## Errors
301	///
302	/// If the response had an error code, or we could not get the file info from
303	/// the file.
304	pub fn try_from_fd_info(mut value: Bytes) -> Result<Self, NetworkParseError> {
305		let rc = value.get_u32();
306		if rc != 0 {
307			return Err(NetworkParseError::ErrorCode(rc));
308		}
309
310		let fd_info = SataFDInfo::try_from(value)?;
311
312		Ok(Self::FDInfo(fd_info))
313	}
314}
315
316impl From<&SataQueryResponse> for Bytes {
317	fn from(value: &SataQueryResponse) -> Self {
318		match value {
319			SataQueryResponse::FDInfo(fd_info) => {
320				let mut buff = BytesMut::with_capacity(88);
321				buff.put_u32(0);
322				buff.extend(Bytes::from(fd_info));
323				buff.freeze()
324			}
325			SataQueryResponse::LargeSize(lorg) => {
326				let mut buff = BytesMut::with_capacity(88);
327				buff.put_u32(0);
328				buff.put_u64(*lorg);
329				buff.extend([0; 76]);
330				buff.freeze()
331			}
332			SataQueryResponse::SmallSize(smol) => {
333				let mut buff = BytesMut::with_capacity(88);
334				buff.put_u32(0);
335				buff.put_u32(*smol);
336				buff.extend([0; 80]);
337				buff.freeze()
338			}
339			SataQueryResponse::ErrorCode(ec) => {
340				let mut buff = BytesMut::with_capacity(88);
341				buff.put_u32(*ec);
342				buff.extend_from_slice(&[0; 84]);
343				buff.freeze()
344			}
345		}
346	}
347}
348
349impl From<SataQueryResponse> for Bytes {
350	fn from(value: SataQueryResponse) -> Self {
351		Self::from(&value)
352	}
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
356/// The info related to the file/directory of the path queried.
357pub struct SataFDInfo {
358	/// The raw underlying flags for the file/directory at the path queried.
359	file_or_folder_flags: u32,
360	/// The permissions bits for the file/directory at the path queried.
361	perms: u32,
362	/// The length of the file if this is an actual file, otherwise it _should_
363	/// be set to 0.
364	file_length: u32,
365	/// A FAT-TS like timestamp for when this path was created.
366	created_timestamp: u64,
367	/// A FAT-TS like timestamp for when this path was last updated.
368	last_updated_timestamp: u64,
369}
370
371impl SataFDInfo {
372	/// File information for a particular file descriptor/path.
373	#[must_use]
374	pub async fn get_info(
375		host_filesystem: &HostFilesystem,
376		metadata: &Metadata,
377		path: &PathBuf,
378	) -> Self {
379		let is_read_only = if metadata.is_dir() {
380			host_filesystem.folder_is_read_only(path).await
381		} else {
382			metadata.permissions().readonly()
383		};
384
385		let file_or_folder_flags = if metadata.is_file() {
386			0x2C00_0000
387		} else {
388			0xAC00_0000
389		};
390		let perms = if is_read_only { 0x444 } else { 0x666 };
391		let file_length = if metadata.is_dir() {
392			0
393		} else {
394			u32::try_from(metadata.len()).unwrap_or(u32::MAX)
395		};
396		let created_timestamp = u64::try_from(
397			metadata
398				.created()
399				.unwrap_or(SystemTime::now())
400				.duration_since(*FAT_TIMESTAMP_START)
401				.unwrap_or(Duration::from_secs(0))
402				.as_millis(),
403		)
404		.unwrap_or(u64::MAX);
405		let updated_timestamp = u64::try_from(
406			metadata
407				.modified()
408				.unwrap_or(SystemTime::now())
409				.duration_since(*FAT_TIMESTAMP_START)
410				.unwrap_or(Duration::from_secs(0))
411				.as_millis(),
412		)
413		.unwrap_or(u64::MAX);
414
415		Self {
416			file_or_folder_flags,
417			perms,
418			file_length,
419			created_timestamp,
420			last_updated_timestamp: updated_timestamp,
421		}
422	}
423
424	#[cfg(feature = "nus")]
425	/// Create a new file descriptor info from NUS metadata.
426	#[must_use]
427	pub async fn new_from_nus(
428		host_filesystem: &HostFilesystem,
429		disk_path: &PathBuf,
430		cafe_content_file_info: Option<CafeContentFileInformation>,
431	) -> Self {
432		let is_read_only = if cafe_content_file_info.is_none() {
433			host_filesystem.folder_is_read_only(disk_path).await
434		} else {
435			false
436		};
437
438		let file_or_folder_flags = if cafe_content_file_info.is_some() {
439			0x2C00_0000
440		} else {
441			0xAC00_0000
442		};
443		let perms = if is_read_only { 0x444 } else { 0x666 };
444		let file_length = if let Some(fi) = cafe_content_file_info.as_ref() {
445			fi.file_size()
446		} else {
447			0_u32
448		};
449		let created_timestamp = u64::try_from(
450			SystemTime::now()
451				.duration_since(*FAT_TIMESTAMP_START)
452				.unwrap_or(Duration::from_secs(0))
453				.as_millis(),
454		)
455		.unwrap_or(u64::MAX);
456		let updated_timestamp = u64::try_from(
457			SystemTime::now()
458				.duration_since(*FAT_TIMESTAMP_START)
459				.unwrap_or(Duration::from_secs(0))
460				.as_millis(),
461		)
462		.unwrap_or(u64::MAX);
463
464		Self {
465			file_or_folder_flags,
466			perms,
467			file_length,
468			created_timestamp,
469			last_updated_timestamp: updated_timestamp,
470		}
471	}
472
473	/// Create a fake fd info from totally controlled values.
474	#[must_use]
475	pub fn create_fake_info(
476		file_or_folder_flags: u32,
477		perms: u32,
478		file_length: u32,
479		created_timestamp: u64,
480		updated_timestamp: u64,
481	) -> Self {
482		Self {
483			file_or_folder_flags,
484			perms,
485			file_length,
486			created_timestamp,
487			last_updated_timestamp: updated_timestamp,
488		}
489	}
490
491	/// Get the raw file or folder type flags for a particular path.
492	#[must_use]
493	pub const fn flags(&self) -> u32 {
494		self.file_or_folder_flags
495	}
496
497	/// Check if this path actually exists on disk.
498	#[must_use]
499	pub const fn exists(&self) -> bool {
500		(self.file_or_folder_flags & 0x2000_0000) != 0
501	}
502
503	/// Check if this path was interpreted as a file.
504	#[must_use]
505	pub const fn is_file(&self) -> bool {
506		(self.file_or_folder_flags & 0x8000_0000) == 0
507	}
508
509	/// Check if this path was interpreted as a directory.
510	#[must_use]
511	pub const fn is_directory(&self) -> bool {
512		!self.is_file()
513	}
514
515	/// Get the unix permissions that exists on this file.
516	///
517	/// Given this is based originally on a windows filesystem, which really only
518	/// has a natural equivalent for read only flags. You will either get
519	/// `0x666`, or `0x444`.
520	#[must_use]
521	pub const fn permissions(&self) -> u32 {
522		self.perms
523	}
524
525	/// The size of a file, if we are actually pointing at a file.
526	#[must_use]
527	pub const fn file_size(&self) -> Option<u32> {
528		if self.is_file() {
529			Some(self.file_length)
530		} else {
531			None
532		}
533	}
534
535	/// A FAT-like timestamp that may be wrapped around.
536	///
537	/// Access the raw underlying value.
538	#[must_use]
539	pub const fn raw_created_timestamp(&self) -> u64 {
540		self.created_timestamp
541	}
542
543	/// A FAT-like timestamp that may be wrapped around.
544	///
545	/// Access the raw underlying value.
546	#[must_use]
547	pub const fn raw_last_updated_timestamp(&self) -> u64 {
548		self.last_updated_timestamp
549	}
550}
551
552impl From<&SataFDInfo> for Bytes {
553	fn from(value: &SataFDInfo) -> Self {
554		let mut buff = BytesMut::with_capacity(84);
555		buff.put_u32(value.file_or_folder_flags);
556		buff.put_u32(value.perms);
557		buff.put_u32(1);
558		buff.put_u32(1);
559		buff.put_u32(value.file_length);
560		buff.put_u32(0);
561		buff.put_u32(0xE8);
562		buff.put_u32(0xDA6F_F000);
563		buff.put_u32(0);
564		buff.put_u64(value.created_timestamp);
565		buff.put_u64(value.last_updated_timestamp);
566		buff.extend_from_slice(&[0; 32]);
567		buff.freeze()
568	}
569}
570
571impl From<SataFDInfo> for Bytes {
572	fn from(value: SataFDInfo) -> Self {
573		Self::from(&value)
574	}
575}
576
577impl TryFrom<Bytes> for SataFDInfo {
578	type Error = NetworkParseError;
579
580	fn try_from(mut value: Bytes) -> Result<Self, Self::Error> {
581		if value.len() < 84 {
582			return Err(NetworkParseError::FieldNotLongEnough(
583				"SataFDInfo",
584				"Body",
585				84,
586				value.len(),
587				value,
588			));
589		}
590		if value.len() > 84 {
591			return Err(NetworkParseError::UnexpectedTrailer(
592				"SataFDInfoBody",
593				value.slice(84..),
594			));
595		}
596
597		let fd_flags = value.get_u32();
598		let unix_perms = value.get_u32();
599		// skip two u32 that should always be 1
600		_ = value.get_u32();
601		_ = value.get_u32();
602		let file_size = value.get_u32();
603		// skip 4 u32's that should be various values
604		_ = value.get_u32();
605		_ = value.get_u32();
606		_ = value.get_u32();
607		_ = value.get_u32();
608		// Get the timestamps.
609		let created_ts = value.get_u64();
610		let updated_ts = value.get_u64();
611		// 32 0's
612
613		Ok(Self {
614			file_or_folder_flags: fd_flags,
615			perms: unix_perms,
616			file_length: file_size,
617			created_timestamp: created_ts,
618			last_updated_timestamp: updated_ts,
619		})
620	}
621}
622
623#[cfg(test)]
624mod unit_tests {
625	use super::*;
626
627	#[test]
628	pub fn query_types_to_and_fro() {
629		for qt in vec![
630			SataQueryType::FreeDiskSpace,
631			SataQueryType::SizeOfFolder,
632			SataQueryType::FileCount,
633			SataQueryType::FileDetails,
634		] {
635			assert_eq!(Ok(qt), SataQueryType::try_from(u32::from(qt)));
636		}
637	}
638}