cat_dev/fsemul/pcfs/sata_proto/
open_file.rs1use crate::{
7 errors::{CatBridgeError, NetworkParseError},
8 fsemul::{
9 host_filesystem::ResolvedLocation,
10 pcfs::{
11 errors::SataProtocolError,
12 sata_proto::{construct_sata_response, SataCommandInfo, SataPacketHeader},
13 },
14 HostFilesystem,
15 },
16};
17use bytes::{BufMut, Bytes, BytesMut};
18use std::ffi::CStr;
19use tokio::fs::{set_permissions, OpenOptions};
20use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
21
22const FS_ERROR: u32 = 0xFFF0_FFE0;
24const PATH_NOT_EXIST_ERROR: u32 = 0xFFF0_FFE9;
30
31#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct SataOpenFilePacketBody {
34 mode_string: String,
36 path: String,
47}
48
49impl SataOpenFilePacketBody {
50 #[must_use]
51 pub fn mode(&self) -> &str {
52 self.mode_string.as_str()
53 }
54 #[must_use]
55 pub fn path(&self) -> &str {
56 self.path.as_str()
57 }
58
59 #[allow(
66 clippy::permissions_set_readonly_false,
68 )]
69 pub async fn handle(
70 &self,
71 request_header: &SataPacketHeader,
72 command_info: &SataCommandInfo,
73 host_filesystem: &HostFilesystem,
74 ) -> Result<Bytes, CatBridgeError> {
75 let Ok(final_location) = host_filesystem.resolve_path(&self.path) else {
76 return Self::construct_error(request_header, PATH_NOT_EXIST_ERROR);
77 };
78 let ResolvedLocation::Filesystem(fs_location) = final_location else {
79 todo!("network shares not yet implemented!")
80 };
81
82 if !host_filesystem.path_allows_writes(fs_location.resolved_path())
85 && (!self.mode_string.contains('r') || self.mode_string.contains('+'))
86 {
87 return Self::construct_error(request_header, FS_ERROR);
88 }
89 let [allow_becoming_write, _, _, _] = command_info.capabilities().1.to_be_bytes();
90 if fs_location.resolved_path().exists() {
92 let Ok(metadata) = fs_location.resolved_path().metadata() else {
93 return Self::construct_error(request_header, FS_ERROR);
94 };
95 let mut perms = metadata.permissions();
96 if perms.readonly() && !self.mode_string.contains('r') && allow_becoming_write == 0 {
97 return Self::construct_error(request_header, FS_ERROR);
98 }
99 perms.set_readonly(false);
100 if set_permissions(fs_location.resolved_path(), perms)
101 .await
102 .is_err()
103 {
104 return Self::construct_error(request_header, FS_ERROR);
105 }
106 }
107
108 let mut options = OpenOptions::new();
110 if self.mode_string.contains('r') {
111 options.read(true);
112 }
113 if self.mode_string.contains('w') {
114 options.write(true).truncate(true).create(true);
115 }
116 if self.mode_string.contains('a') {
117 options.write(true).truncate(false).create(true);
118 }
119 if self.mode_string.contains('+') {
120 options.create(true);
121 }
122
123 let Ok(fd) = host_filesystem
124 .open_file(options, fs_location.resolved_path())
125 .await
126 else {
127 return Self::construct_error(request_header, FS_ERROR);
128 };
129
130 let mut buff = BytesMut::with_capacity(8);
131 buff.put_u32(0);
132 buff.put_i32(fd);
133 Ok(construct_sata_response(request_header, 0, buff.freeze())?)
134 }
135
136 fn construct_error(
137 packet_header: &SataPacketHeader,
138 error_code: u32,
139 ) -> Result<Bytes, CatBridgeError> {
140 let mut buff = BytesMut::with_capacity(8);
141 buff.put_u32(error_code);
142 buff.put_u32(0);
143
144 Ok(construct_sata_response(packet_header, 0, buff.freeze())?)
145 }
146}
147
148impl TryFrom<Bytes> for SataOpenFilePacketBody {
149 type Error = NetworkParseError;
150
151 fn try_from(value: Bytes) -> Result<Self, Self::Error> {
152 if value.len() < 0x210 {
153 return Err(NetworkParseError::FieldNotLongEnough(
154 "SataOpenFile",
155 "Body",
156 0x210,
157 value.len(),
158 value,
159 ));
160 }
161 if value.len() > 0x210 {
162 return Err(NetworkParseError::UnexpectedTrailer(
163 "SataOpenFile",
164 value.slice(0x210..),
165 ));
166 }
167
168 let (mode_bytes, path_bytes) = value.split_at(0x10);
169 let mode_c_str =
170 CStr::from_bytes_until_nul(mode_bytes).map_err(NetworkParseError::BadCString)?;
171 let path_c_str =
172 CStr::from_bytes_until_nul(path_bytes).map_err(NetworkParseError::BadCString)?;
173 let final_mode = mode_c_str.to_str()?.to_owned();
174 for (idx, car) in final_mode.chars().enumerate() {
175 if idx == 0 && !['r', 'w', 'a'].contains(&car) {
176 return Err(SataProtocolError::BadModeString(final_mode).into());
177 }
178 if idx > 2 {
179 return Err(SataProtocolError::BadModeString(final_mode).into());
180 }
181 if idx != 0 && !['b', '+'].contains(&car) {
182 return Err(SataProtocolError::BadModeString(final_mode).into());
183 }
184 }
185
186 Ok(Self {
187 mode_string: final_mode,
188 path: path_c_str.to_str()?.to_owned(),
189 })
190 }
191}
192
193const SATA_OPEN_FILE_PACKET_BODY_FIELDS: &[NamedField<'static>] = &[NamedField::new("path")];
194
195impl Structable for SataOpenFilePacketBody {
196 fn definition(&self) -> StructDef<'_> {
197 StructDef::new_static(
198 "SataOpenFilePacketBody",
199 Fields::Named(SATA_OPEN_FILE_PACKET_BODY_FIELDS),
200 )
201 }
202}
203
204impl Valuable for SataOpenFilePacketBody {
205 fn as_value(&self) -> Value<'_> {
206 Value::Structable(self)
207 }
208
209 fn visit(&self, visitor: &mut dyn Visit) {
210 visitor.visit_named_fields(&NamedValues::new(
211 SATA_OPEN_FILE_PACKET_BODY_FIELDS,
212 &[Valuable::as_value(&self.path)],
213 ));
214 }
215}
216
217#[cfg(test)]
218mod unit_tests {
219 use super::*;
220 use crate::fsemul::host_filesystem::test_helpers::{
221 create_temporary_host_filesystem, join_many,
222 };
223
224 #[tokio::test]
225 pub async fn simple_open_file_request() {
226 let (tempdir, fs) = create_temporary_host_filesystem().await;
227 let request = SataOpenFilePacketBody {
228 path: "/%SLC_EMU_DIR/to-query/file.txt".to_owned(),
229 mode_string: "r".to_owned(),
230 };
231 let mocked_header = SataPacketHeader {
232 packet_data_len: 0,
233 packet_id: 0,
234 flags: 0,
235 version: 0,
236 timestamp_on_host: 0,
237 pid_on_host: 0,
238 };
239 let mocked_command_info = SataCommandInfo {
240 user: (0, 0),
241 capabilities: (0, 0),
242 command: 0x5,
243 };
244
245 let base_dir = join_many(tempdir.path(), ["data", "slc", "to-query"]);
246 tokio::fs::create_dir(&base_dir)
247 .await
248 .expect("Failed to create temporary directory for test!");
249 tokio::fs::write(join_many(&base_dir, ["file.txt"]), vec![0; 1307])
250 .await
251 .expect("Failed to write test file!");
252
253 let mut response = request
254 .handle(&mocked_header, &mocked_command_info, &fs)
255 .await
256 .expect("Failed to handle change mode!");
257 assert_eq!(response.len(), 8 + 0x20, "Packet is not correct size!");
258 _ = response.split_to(0x20);
260 assert_eq!(
261 &response[..4],
262 &[0x00, 0x00, 0x00, 0x00], );
264 assert_ne!(
265 &response[4..],
266 &[0x00, 0x00, 0x00, 0x00], );
268 fs.close_file(i32::from_be_bytes([
269 response[4],
270 response[5],
271 response[6],
272 response[7],
273 ]))
274 .await;
275 }
276}