1use crate::api;
2use crate::error::{Error, ErrorCode, Result};
3use crate::resource::{Handle, IO_FLAG_EOF, MAX_IO_CHUNK_BYTES};
4use serde::{Deserialize, Serialize};
5use std::sync::atomic::{AtomicU64, Ordering};
6
7static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
8const ATOMIC_TEMP_ATTEMPTS: usize = 16;
9
10#[derive(Debug, Clone, Default, Serialize)]
11pub struct OpenOptions {
12 pub read: bool,
13 pub write: bool,
14 pub create: bool,
15 pub create_new: bool,
16 pub truncate: bool,
17 pub append: bool,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub mode: Option<u32>,
20}
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct FileStat {
24 pub uri: String,
25 pub kind: FileKind,
26 pub size: u64,
27 pub mode: u32,
28 pub modified_unix_ms: i64,
29 #[serde(default)]
30 pub created_unix_ms: Option<i64>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum FileKind {
36 File,
37 Directory,
38 Symlink,
39 Other,
40 #[serde(other)]
41 Unknown,
42}
43
44#[derive(Debug, Clone, Deserialize)]
45pub struct DirectoryEntry {
46 pub name: String,
47 pub uri: String,
48 pub kind: FileKind,
49}
50
51#[derive(Debug, Clone, Deserialize)]
52pub struct DirectoryPage {
53 #[serde(default)]
54 pub entries: Vec<DirectoryEntry>,
55 pub eof: bool,
56}
57
58#[derive(Debug, Clone, Deserialize)]
59pub struct WatchEvent {
60 pub sequence: u64,
61 pub kind: WatchKind,
62 pub uri: String,
63 #[serde(default)]
64 pub previous_uri: Option<String>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum WatchKind {
70 Create,
71 Change,
72 Delete,
73 Rename,
74 Overflow,
75 #[serde(other)]
76 Unknown,
77}
78
79#[derive(Debug, Clone, Deserialize)]
80pub struct MountInfo {
81 pub id: String,
82 pub uri: String,
83 pub read_only: bool,
84}
85
86#[derive(Debug, Clone, Deserialize)]
87pub struct Mounts {
88 #[serde(default)]
89 pub mounts: Vec<MountInfo>,
90}
91
92#[derive(Deserialize)]
93struct HandleResult {
94 handle: u64,
95}
96
97#[derive(Serialize)]
98struct URIArguments<'a> {
99 uri: &'a str,
100}
101
102pub struct File {
103 handle: Handle,
104}
105
106impl File {
107 pub fn open(uri: &str, options: OpenOptions) -> Result<Self> {
108 #[derive(Serialize)]
109 struct Arguments<'a> {
110 uri: &'a str,
111 options: OpenOptions,
112 }
113 let opened: HandleResult = api::call("fs.open", &Arguments { uri, options })?;
114 Ok(Self {
115 handle: Handle::new(opened.handle)?,
116 })
117 }
118
119 pub fn read(&mut self, capacity: usize) -> Result<(Vec<u8>, u32)> {
120 self.handle.read(capacity)
121 }
122
123 pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
124 write_file_chunks(bytes, |chunk| self.handle.write(chunk, 0))
125 }
126
127 pub fn seek(&mut self, offset: i64, whence: u32) -> Result<u64> {
128 self.handle.seek(offset, whence)
129 }
130
131 pub fn sync(&mut self) -> Result<()> {
132 #[derive(Serialize)]
133 struct Arguments {
134 handle: u64,
135 }
136 let _: serde_json::Value = api::call(
137 "fs.sync",
138 &Arguments {
139 handle: self.handle.id(),
140 },
141 )?;
142 Ok(())
143 }
144
145 pub fn close(mut self) -> Result<()> {
146 self.handle.close()
147 }
148}
149
150fn write_file_chunks(bytes: &[u8], mut write: impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
151 let mut acknowledged = 0;
152 for chunk in bytes.chunks(MAX_IO_CHUNK_BYTES) {
153 if let Err(mut error) = write(chunk) {
154 if !error.details.is_object() {
157 error.details = serde_json::json!({});
158 }
159 error.details["mutation_outcome"] = serde_json::json!("unknown");
160 error.details["acknowledged_bytes"] = serde_json::json!(acknowledged);
161 error.retryable = false;
162 return Err(error);
163 }
164 acknowledged += chunk.len();
165 }
166 Ok(())
167}
168
169pub struct Directory {
170 handle: Handle,
171}
172
173impl Directory {
174 pub fn open(uri: &str) -> Result<Self> {
175 let opened: HandleResult = api::call("fs.read_dir.open", &URIArguments { uri })?;
176 Ok(Self {
177 handle: Handle::new(opened.handle)?,
178 })
179 }
180
181 pub fn next(&mut self, limit: u16) -> Result<DirectoryPage> {
182 #[derive(Serialize)]
183 struct Arguments {
184 handle: u64,
185 limit: u16,
186 }
187 api::call(
188 "fs.read_dir.next",
189 &Arguments {
190 handle: self.handle.id(),
191 limit,
192 },
193 )
194 }
195
196 pub fn close(mut self) -> Result<()> {
197 self.handle.close()
198 }
199}
200
201pub struct Watch {
202 handle: Handle,
203}
204
205impl Watch {
206 pub fn open(uri: &str) -> Result<Self> {
207 #[derive(Serialize)]
208 struct Arguments<'a> {
209 uri: &'a str,
210 recursive: bool,
211 }
212 let opened: HandleResult = api::call(
213 "fs.watch",
214 &Arguments {
215 uri,
216 recursive: false,
217 },
218 )?;
219 Ok(Self {
220 handle: Handle::new(opened.handle)?,
221 })
222 }
223
224 pub fn next(&mut self, timeout_ms: u32) -> Result<WatchEvent> {
225 #[derive(Serialize)]
226 struct Arguments {
227 handle: u64,
228 timeout_ms: u32,
229 }
230 api::call(
231 "fs.watch_next",
232 &Arguments {
233 handle: self.handle.id(),
234 timeout_ms,
235 },
236 )
237 }
238
239 pub fn close(mut self) -> Result<()> {
240 self.handle.close()
241 }
242}
243
244pub fn mounts() -> Result<Mounts> {
245 api::call("fs.mounts", &serde_json::json!({}))
246}
247
248pub fn stat(uri: &str, follow_symlinks: bool) -> Result<FileStat> {
249 #[derive(Serialize)]
250 struct Arguments<'a> {
251 uri: &'a str,
252 follow_symlinks: bool,
253 }
254 api::call(
255 "fs.stat",
256 &Arguments {
257 uri,
258 follow_symlinks,
259 },
260 )
261}
262
263pub fn read_file(uri: &str) -> Result<Vec<u8>> {
264 let mut file = File::open(
265 uri,
266 OpenOptions {
267 read: true,
268 ..OpenOptions::default()
269 },
270 )?;
271 let mut result = Vec::new();
272 loop {
273 let (chunk, flags) = file.read(MAX_IO_CHUNK_BYTES)?;
274 if chunk.is_empty() && flags & IO_FLAG_EOF == 0 {
275 return Err(Error::internal("file read made no progress"));
276 }
277 result.extend_from_slice(&chunk);
278 if flags & IO_FLAG_EOF != 0 {
279 file.close()?;
280 return Ok(result);
281 }
282 }
283}
284
285pub fn read_text(uri: &str) -> Result<String> {
286 String::from_utf8(read_file(uri)?)
287 .map_err(|_| Error::internal("file content is not valid UTF-8"))
288}
289
290pub fn write_file(uri: &str, bytes: &[u8], atomic: bool) -> Result<()> {
291 if !atomic {
292 return write_direct(uri, bytes, false);
293 }
294 for _ in 0..ATOMIC_TEMP_ATTEMPTS {
295 let temporary = atomic_temporary_uri(uri)?;
296 match write_direct(&temporary, bytes, true) {
297 Ok(()) => {
298 let result = rename(&temporary, uri, true);
299 if result.is_err() {
300 let _ = remove(&temporary, false);
301 }
302 return result;
303 }
304 Err(error) if error.code == ErrorCode::AlreadyExists => continue,
305 Err(error) => {
306 let _ = remove(&temporary, false);
307 return Err(error);
308 }
309 }
310 }
311 Err(Error::internal(
312 "could not allocate a unique same-directory atomic write file",
313 ))
314}
315
316pub fn write_text(uri: &str, text: &str, atomic: bool) -> Result<()> {
317 write_file(uri, text.as_bytes(), atomic)
318}
319
320fn write_direct(uri: &str, bytes: &[u8], create_new: bool) -> Result<()> {
321 let mut file = File::open(
322 uri,
323 OpenOptions {
324 write: true,
325 create: !create_new,
326 create_new,
327 truncate: !create_new,
328 mode: Some(0o600),
329 ..OpenOptions::default()
330 },
331 )?;
332 file.write_all(bytes)?;
333 file.sync()?;
334 file.close()
335}
336
337pub fn remove(uri: &str, recursive: bool) -> Result<()> {
338 #[derive(Serialize)]
339 struct Arguments<'a> {
340 uri: &'a str,
341 recursive: bool,
342 }
343 let _: serde_json::Value = api::call("fs.remove", &Arguments { uri, recursive })?;
344 Ok(())
345}
346
347pub fn mkdir(uri: &str, recursive: bool, mode: u32) -> Result<()> {
348 #[derive(Serialize)]
349 struct Arguments<'a> {
350 uri: &'a str,
351 recursive: bool,
352 mode: u32,
353 }
354 let _: serde_json::Value = api::call(
355 "fs.mkdir",
356 &Arguments {
357 uri,
358 recursive,
359 mode,
360 },
361 )?;
362 Ok(())
363}
364
365pub fn rename(from: &str, to: &str, overwrite: bool) -> Result<()> {
366 transfer("fs.rename", from, to, overwrite)
367}
368
369pub fn copy(from: &str, to: &str, overwrite: bool) -> Result<()> {
370 transfer("fs.copy", from, to, overwrite)
371}
372
373fn transfer(operation: &str, from: &str, to: &str, overwrite: bool) -> Result<()> {
374 #[derive(Serialize)]
375 struct Arguments<'a> {
376 from: &'a str,
377 to: &'a str,
378 overwrite: bool,
379 }
380 let _: serde_json::Value = api::call(
381 operation,
382 &Arguments {
383 from,
384 to,
385 overwrite,
386 },
387 )?;
388 Ok(())
389}
390
391pub fn set_times(uri: &str, accessed_unix_ms: i64, modified_unix_ms: i64) -> Result<()> {
392 #[derive(Serialize)]
393 struct Arguments<'a> {
394 uri: &'a str,
395 accessed_unix_ms: i64,
396 modified_unix_ms: i64,
397 }
398 let _: serde_json::Value = api::call(
399 "fs.set_times",
400 &Arguments {
401 uri,
402 accessed_unix_ms,
403 modified_unix_ms,
404 },
405 )?;
406 Ok(())
407}
408
409fn atomic_temporary_uri(uri: &str) -> Result<String> {
410 let (directory, name) = uri
411 .rsplit_once('/')
412 .filter(|(_, name)| !name.is_empty())
413 .ok_or_else(|| Error::from_abi_status(-1))?;
414 let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
415 Ok(format!(
416 "{directory}/.{name}.redevplugin-{sequence:016x}.tmp"
417 ))
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn chunk_failure_preserves_acknowledged_bytes_and_never_replays() {
426 let bytes = vec![0; MAX_IO_CHUNK_BYTES + 1];
427 let mut calls = 0;
428 let error = write_file_chunks(&bytes, |_| {
429 calls += 1;
430 if calls == 1 {
431 Ok(())
432 } else {
433 Err(Error::from_abi_status(-2))
434 }
435 })
436 .unwrap_err();
437 assert_eq!(calls, 2);
438 assert_eq!(error.code, ErrorCode::PermissionDenied);
439 assert_eq!(error.details["mutation_outcome"], "unknown");
440 assert_eq!(error.details["acknowledged_bytes"], MAX_IO_CHUNK_BYTES);
441 assert!(!error.retryable);
442 }
443
444 #[test]
445 fn atomic_temporary_file_stays_in_the_same_directory() {
446 let temporary = atomic_temporary_uri("redevfs://workspace/src/data.bin").unwrap();
447 assert!(temporary.starts_with("redevfs://workspace/src/.data.bin.redevplugin-"));
448 assert!(temporary.ends_with(".tmp"));
449 }
450}