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