Skip to main content

Mountain/Environment/FileSystemProvider/
ReadOperations.rs

1//! # FileSystemProvider - Read Operations
2//!
3//! Implements [`FileSystemReader`](CommonLibrary::FileSystem::FileSystemReader)
4//! for [`MountainEnvironment`]. All three functions call
5//! `Utility::PathSecurity::IsPathAllowedForAccess` first, which enforces
6//! workspace-trust rules and prevents path-traversal escapes.
7//!
8//! ## Functions
9//!
10//! - `read_file_impl` - validates the path is a regular file (not a directory),
11//!   then reads bytes via `tokio::fs::read`.
12//! - `stat_file_impl` - returns a `FileSystemStatDTO` with file type flags
13//!   (File / Directory / SymbolicLink bitmask), mtime, ctime, and size. Symlink
14//!   detection uses a second `symlink_metadata` call because `metadata` follows
15//!   links. `Permissions` is currently `None`; see the inline comment for the
16//!   Windows / Unix implementation plan. `CreationTime` falls back to `0` on
17//!   platforms that don't expose it (e.g. Linux).
18//! - `read_directory_impl` - validates the path is a directory, streams entries
19//!   via `tokio::fs::read_dir`, and classifies each entry as `File`,
20//!   `Directory`, `SymbolicLink`, or `Unknown`.
21
22use std::path::PathBuf;
23
24use CommonLibrary::{
25	Error::CommonError::CommonError,
26	FileSystem::DTO::{FileSystemStatDTO::FileSystemStatDTO, FileTypeDTO::FileTypeDTO},
27};
28use tokio::fs;
29
30use super::super::{MountainEnvironment::MountainEnvironment, Utility};
31
32/// Read operations implementation for MountainEnvironment
33pub(super) async fn read_file_impl(env:&MountainEnvironment, path:&PathBuf) -> Result<Vec<u8>, CommonError> {
34	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, path)?;
35
36	// Validate that the path exists and is a file, not a directory
37	let metadata = fs::metadata(path)
38		.await
39		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "ReadFile.Stat"))?;
40
41	if metadata.is_dir() {
42		return Err(CommonError::InvalidArgument {
43			ArgumentName:"Path".to_string(),
44			Reason:format!("Cannot read directory as file: {}", path.display()),
45		});
46	}
47
48	fs::read(path)
49		.await
50		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "ReadFile"))
51}
52
53/// Stat operations implementation for MountainEnvironment
54pub(super) async fn stat_file_impl(env:&MountainEnvironment, path:&PathBuf) -> Result<FileSystemStatDTO, CommonError> {
55	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, path)?;
56
57	let metadata = fs::metadata(path)
58		.await
59		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "StatFile"))?;
60
61	let mut file_type = 0_u8;
62
63	if metadata.is_file() {
64		file_type |= FileTypeDTO::File as u8;
65	}
66
67	if metadata.is_dir() {
68		file_type |= FileTypeDTO::Directory as u8;
69	}
70
71	// Check for symbolic link separately using symlink_metadata()
72	let file_type_raw = fs::symlink_metadata(path)
73		.await
74		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "StatFile.FileType"))?;
75
76	if file_type_raw.is_symlink() {
77		file_type |= FileTypeDTO::SymbolicLink as u8;
78	}
79
80	// Note: Windows typically doesn't support creation_time, handle gracefully
81	let get_milli_timestamp = |system_time_result:Result<std::time::SystemTime, _>| -> u64 {
82		system_time_result
83			.ok()
84			.and_then(|time| time.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
85			.map_or(0, |duration| duration.as_millis() as u64)
86	};
87
88	Ok(FileSystemStatDTO {
89		FileType:file_type,
90
91		CreationTime:get_milli_timestamp(metadata.created()),
92
93		ModificationTime:get_milli_timestamp(metadata.modified()),
94
95		Size:metadata.len(),
96
97		// Capture file permissions by extracting Unix file mode (st_mode) and Windows
98		// file attributes. On Unix, extract permission bits (rwx for owner/group/others)
99		// and store in FileSystemPermissionsDTO. On Windows, capture attributes
100		// (readonly, hidden, system, archive). This enables preserving permissions
101		// during file operations and respecting the user's filesystem ACLs. Currently
102		// returns None, which defaults to inherited permissions.
103		Permissions:None,
104	})
105}
106
107/// ReadDirectory operations implementation for MountainEnvironment
108pub(super) async fn read_directory_impl(
109	env:&MountainEnvironment,
110
111	path:&PathBuf,
112) -> Result<Vec<(String, FileTypeDTO)>, CommonError> {
113	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, path)?;
114
115	// Validate that the path exists and is a directory
116	let metadata = fs::metadata(path)
117		.await
118		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "ReadDirectory.Stat"))?;
119
120	if !metadata.is_dir() {
121		return Err(CommonError::InvalidArgument {
122			ArgumentName:"Path".to_string(),
123			Reason:format!("Cannot read directory: path is not a directory: {}", path.display()),
124		});
125	}
126
127	let mut entries = Vec::new();
128
129	let mut read_dir = fs::read_dir(path)
130		.await
131		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "ReadDirectory"))?;
132
133	while let Some(entry_result) = read_dir
134		.next_entry()
135		.await
136		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "ReadDirectory.NextEntry"))?
137	{
138		let file_name = entry_result.file_name().to_string_lossy().into_owned();
139
140		// Determine file type including symbolic link detection
141		let file_type = match entry_result.file_type().await {
142			Ok(ft) => {
143				if ft.is_symlink() {
144					FileTypeDTO::SymbolicLink
145				} else if ft.is_dir() {
146					FileTypeDTO::Directory
147				} else if ft.is_file() {
148					FileTypeDTO::File
149				} else {
150					FileTypeDTO::Unknown
151				}
152			},
153
154			Err(_) => FileTypeDTO::Unknown,
155		};
156
157		entries.push((file_name, file_type));
158	}
159
160	Ok(entries)
161}