1use crate::model::filesystem::{
6 DirEntry, EntryType, FileMetadata, FilePermissions, FileReader, FileSystem, FileWriter, WriteOp,
7};
8use crate::services::remote::channel::{AgentChannel, ChannelError};
9use crate::services::remote::protocol::{
10 append_params, count_lf_params, decode_base64, ls_params, patch_params, read_params,
11 stat_params, sudo_write_params, truncate_params, write_params, PatchOp, RemoteDirEntry,
12 RemoteMetadata,
13};
14use std::io::{self, Cursor, Read, Seek, Write};
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::{Duration, UNIX_EPOCH};
18
19pub struct RemoteFileSystem {
21 channel: Arc<AgentChannel>,
22 connection_string: String,
24}
25
26impl RemoteFileSystem {
27 pub fn new(channel: Arc<AgentChannel>, connection_string: String) -> Self {
29 Self {
30 channel,
31 connection_string,
32 }
33 }
34
35 pub fn connection_string(&self) -> &str {
37 &self.connection_string
38 }
39
40 pub fn is_connected(&self) -> bool {
42 self.channel.is_connected()
43 }
44
45 fn parse_temp_dir_from_info(info: Option<&serde_json::Value>) -> PathBuf {
48 info.and_then(|r| {
49 r.get("temp_dir")
50 .and_then(|v| v.as_str())
51 .map(PathBuf::from)
52 })
53 .unwrap_or_else(|| PathBuf::from("/tmp"))
54 }
55
56 fn to_io_error(e: ChannelError) -> io::Error {
58 match e {
59 ChannelError::Io(e) => e,
60 ChannelError::Remote(msg) => {
61 let kind = if msg.contains("not found") || msg.contains("No such file") {
62 io::ErrorKind::NotFound
63 } else if msg.contains("permission denied") {
64 io::ErrorKind::PermissionDenied
65 } else if msg.contains("is a directory") {
66 io::ErrorKind::IsADirectory
67 } else if msg.contains("not a directory") {
68 io::ErrorKind::NotADirectory
69 } else {
70 io::ErrorKind::Other
71 };
72 io::Error::new(kind, msg)
73 }
74 e => io::Error::other(e.to_string()),
75 }
76 }
77
78 fn convert_metadata(rm: &RemoteMetadata, name: &str) -> FileMetadata {
80 let modified = if rm.mtime > 0 {
81 Some(UNIX_EPOCH + Duration::from_secs(rm.mtime as u64))
82 } else {
83 None
84 };
85
86 let is_hidden = name.starts_with('.');
87 let permissions = FilePermissions::from_mode(rm.mode);
88
89 #[cfg(unix)]
90 let is_readonly = {
91 let (euid, user_groups) =
92 crate::model::filesystem::StdFileSystem::current_user_groups();
93 permissions.is_readonly_for_user(euid, rm.uid, rm.gid, &user_groups)
94 };
95 #[cfg(not(unix))]
96 let is_readonly = permissions.is_readonly();
97
98 let mut meta = FileMetadata::new(rm.size)
99 .with_hidden(is_hidden)
100 .with_readonly(is_readonly)
101 .with_permissions(permissions);
102
103 if let Some(m) = modified {
104 meta = meta.with_modified(m);
105 }
106
107 #[cfg(unix)]
108 {
109 meta.uid = Some(rm.uid);
110 meta.gid = Some(rm.gid);
111 }
112
113 meta
114 }
115
116 fn convert_dir_entry(re: &RemoteDirEntry) -> DirEntry {
118 let entry_type = if re.link {
119 EntryType::Symlink
120 } else if re.dir {
121 EntryType::Directory
122 } else {
123 EntryType::File
124 };
125
126 let modified = if re.mtime > 0 {
127 Some(UNIX_EPOCH + Duration::from_secs(re.mtime as u64))
128 } else {
129 None
130 };
131
132 let is_hidden = re.name.starts_with('.');
133 let permissions = FilePermissions::from_mode(re.mode);
134 let is_readonly = permissions.is_readonly();
135
136 let mut metadata = FileMetadata::new(re.size)
137 .with_hidden(is_hidden)
138 .with_readonly(is_readonly)
139 .with_permissions(permissions);
140
141 if let Some(m) = modified {
142 metadata = metadata.with_modified(m);
143 }
144
145 let mut entry = DirEntry::new(PathBuf::from(&re.path), re.name.clone(), entry_type);
146 entry.metadata = Some(metadata);
147 entry.symlink_target_is_dir = re.link_dir;
148
149 entry
150 }
151}
152
153impl FileSystem for RemoteFileSystem {
154 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
155 let path_str = path.to_string_lossy();
156 let (data_chunks, _result) = self
157 .channel
158 .request_with_data_blocking("read", read_params(&path_str, None, None))
159 .map_err(Self::to_io_error)?;
160
161 let mut content = Vec::new();
163 for chunk in data_chunks {
164 if let Some(b64) = chunk.get("data").and_then(|v| v.as_str()) {
165 let decoded = decode_base64(b64)
166 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
167 content.extend(decoded);
168 }
169 }
170
171 Ok(content)
172 }
173
174 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
175 let path_str = path.to_string_lossy();
176 let (data_chunks, result) = self
177 .channel
178 .request_with_data_blocking("read", read_params(&path_str, Some(offset), Some(len)))
179 .map_err(Self::to_io_error)?;
180
181 let mut content = Vec::new();
183 for chunk in data_chunks {
184 if let Some(b64) = chunk.get("data").and_then(|v| v.as_str()) {
185 let decoded = decode_base64(b64)
186 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
187 content.extend(decoded);
188 }
189 }
190
191 let agent_reported_size = result
193 .get("size")
194 .and_then(|v| v.as_u64())
195 .map(|s| s as usize);
196
197 if content.len() != len {
201 return Err(io::Error::new(
202 io::ErrorKind::UnexpectedEof,
203 format!(
204 "read_range: expected {} bytes at offset {}, got {} (agent reported: {:?}, path: {})",
205 len,
206 offset,
207 content.len(),
208 agent_reported_size,
209 path_str
210 ),
211 ));
212 }
213
214 Ok(content)
215 }
216
217 fn count_line_feeds_in_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<usize> {
218 let path_str = path.to_string_lossy();
219 let result = self
220 .channel
221 .request_blocking("count_lf", count_lf_params(&path_str, offset, len))
222 .map_err(Self::to_io_error)?;
223
224 result
225 .get("count")
226 .and_then(|v| v.as_u64())
227 .map(|c| c as usize)
228 .ok_or_else(|| {
229 io::Error::new(
230 io::ErrorKind::InvalidData,
231 "missing count in count_lf response",
232 )
233 })
234 }
235
236 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
237 let path_str = path.to_string_lossy();
238 self.channel
239 .request_blocking("write", write_params(&path_str, data))
240 .map_err(Self::to_io_error)?;
241 Ok(())
242 }
243
244 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
245 self.write_file(path, &[])?;
247 Ok(Box::new(RemoteFileWriter::new(
248 self.channel.clone(),
249 path.to_path_buf(),
250 )))
251 }
252
253 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
254 let data = self.read_file(path)?;
256 Ok(Box::new(RemoteFileReader::new(data)))
257 }
258
259 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
260 Ok(Box::new(RemoteFileWriter::new(
261 self.channel.clone(),
262 path.to_path_buf(),
263 )))
264 }
265
266 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
267 Ok(Box::new(AppendingRemoteFileWriter::new(
269 self.channel.clone(),
270 path.to_path_buf(),
271 )))
272 }
273
274 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
275 let path_str = path.to_string_lossy();
276 self.channel
277 .request_blocking("truncate", truncate_params(&path_str, len))
278 .map_err(Self::to_io_error)?;
279 Ok(())
280 }
281
282 fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
283 let patch_ops: Vec<PatchOp> = ops
285 .iter()
286 .map(|op| match op {
287 WriteOp::Copy { offset, len } => PatchOp::copy(*offset, *len),
288 WriteOp::Insert { data } => PatchOp::insert(data),
289 })
290 .collect();
291
292 let src_str = src_path.to_string_lossy();
293 let dst_str = dst_path.to_string_lossy();
294 let dst_param = if src_path == dst_path {
295 None
296 } else {
297 Some(dst_str.as_ref())
298 };
299
300 self.channel
301 .request_blocking("patch", patch_params(&src_str, dst_param, &patch_ops))
302 .map_err(Self::to_io_error)?;
303 Ok(())
304 }
305
306 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
307 let params = serde_json::json!({
308 "from": from.to_string_lossy(),
309 "to": to.to_string_lossy()
310 });
311 self.channel
312 .request_blocking("mv", params)
313 .map_err(Self::to_io_error)?;
314 Ok(())
315 }
316
317 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
318 let params = serde_json::json!({
319 "from": from.to_string_lossy(),
320 "to": to.to_string_lossy()
321 });
322 let result = self
323 .channel
324 .request_blocking("cp", params)
325 .map_err(Self::to_io_error)?;
326
327 Ok(result.get("size").and_then(|v| v.as_u64()).unwrap_or(0))
328 }
329
330 fn remove_file(&self, path: &Path) -> io::Result<()> {
331 let params = serde_json::json!({"path": path.to_string_lossy()});
332 self.channel
333 .request_blocking("rm", params)
334 .map_err(Self::to_io_error)?;
335 Ok(())
336 }
337
338 fn remove_dir(&self, path: &Path) -> io::Result<()> {
339 let params = serde_json::json!({"path": path.to_string_lossy()});
340 self.channel
341 .request_blocking("rmdir", params)
342 .map_err(Self::to_io_error)?;
343 Ok(())
344 }
345
346 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
347 let path_str = path.to_string_lossy();
348 let result = self
349 .channel
350 .request_blocking("stat", stat_params(&path_str, true))
351 .map_err(Self::to_io_error)?;
352
353 let rm: RemoteMetadata = serde_json::from_value(result)
354 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
355
356 let name = path
357 .file_name()
358 .map(|n| n.to_string_lossy().to_string())
359 .unwrap_or_default();
360 Ok(Self::convert_metadata(&rm, &name))
361 }
362
363 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
364 let path_str = path.to_string_lossy();
365 let result = self
366 .channel
367 .request_blocking("stat", stat_params(&path_str, false))
368 .map_err(Self::to_io_error)?;
369
370 let rm: RemoteMetadata = serde_json::from_value(result)
371 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
372
373 let name = path
374 .file_name()
375 .map(|n| n.to_string_lossy().to_string())
376 .unwrap_or_default();
377 Ok(Self::convert_metadata(&rm, &name))
378 }
379
380 fn is_dir(&self, path: &Path) -> io::Result<bool> {
381 let path_str = path.to_string_lossy();
382 let result = self
383 .channel
384 .request_blocking("stat", stat_params(&path_str, true))
385 .map_err(Self::to_io_error)?;
386
387 Ok(result.get("dir").and_then(|v| v.as_bool()).unwrap_or(false))
388 }
389
390 fn is_file(&self, path: &Path) -> io::Result<bool> {
391 let path_str = path.to_string_lossy();
392 let result = self
393 .channel
394 .request_blocking("stat", stat_params(&path_str, true))
395 .map_err(Self::to_io_error)?;
396
397 Ok(result
398 .get("file")
399 .and_then(|v| v.as_bool())
400 .unwrap_or(false))
401 }
402
403 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
404 #[cfg(unix)]
405 {
406 let params = serde_json::json!({
407 "path": path.to_string_lossy(),
408 "mode": permissions.mode()
409 });
410 self.channel
411 .request_blocking("chmod", params)
412 .map_err(Self::to_io_error)?;
413 }
414 #[cfg(not(unix))]
415 {
416 let _ = (path, permissions);
417 }
418 Ok(())
419 }
420
421 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
422 let path_str = path.to_string_lossy();
423 let result = self
424 .channel
425 .request_blocking("ls", ls_params(&path_str))
426 .map_err(Self::to_io_error)?;
427
428 let entries: Vec<RemoteDirEntry> = result
429 .get("entries")
430 .and_then(|v| serde_json::from_value(v.clone()).ok())
431 .unwrap_or_default();
432
433 Ok(entries.iter().map(Self::convert_dir_entry).collect())
434 }
435
436 fn create_dir(&self, path: &Path) -> io::Result<()> {
437 let params = serde_json::json!({"path": path.to_string_lossy()});
438 self.channel
439 .request_blocking("mkdir", params)
440 .map_err(Self::to_io_error)?;
441 Ok(())
442 }
443
444 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
445 let params = serde_json::json!({
446 "path": path.to_string_lossy(),
447 "parents": true
448 });
449 self.channel
450 .request_blocking("mkdir", params)
451 .map_err(Self::to_io_error)?;
452 Ok(())
453 }
454
455 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
456 let params = serde_json::json!({"path": path.to_string_lossy()});
457 let result = self
458 .channel
459 .request_blocking("realpath", params)
460 .map_err(Self::to_io_error)?;
461
462 let canonical = result.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
463 io::Error::new(io::ErrorKind::InvalidData, "missing path in response")
464 })?;
465
466 Ok(PathBuf::from(canonical))
467 }
468
469 fn current_uid(&self) -> u32 {
470 0
473 }
474
475 fn remote_connection_info(&self) -> Option<&str> {
476 Some(&self.connection_string)
477 }
478
479 fn is_remote_connected(&self) -> bool {
480 self.channel.is_connected()
481 }
482
483 fn remote_channel_id(&self) -> Option<u64> {
484 Some(self.channel.id())
485 }
486
487 fn remote_reconnect_notify(&self) -> Option<std::sync::Arc<tokio::sync::Notify>> {
488 Some(self.channel.reconnect_notify())
489 }
490
491 fn home_dir(&self) -> io::Result<PathBuf> {
492 let result = self
493 .channel
494 .request_blocking("info", serde_json::json!({}))
495 .map_err(Self::to_io_error)?;
496
497 let home = result.get("home").and_then(|v| v.as_str()).ok_or_else(|| {
498 io::Error::new(io::ErrorKind::InvalidData, "missing home in response")
499 })?;
500
501 Ok(PathBuf::from(home))
502 }
503
504 fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
505 let temp_dir = Self::parse_temp_dir_from_info(
509 self.channel
510 .request_blocking("info", serde_json::json!({}))
511 .ok()
512 .as_ref(),
513 );
514 let file_name = dest_path
515 .file_name()
516 .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
517 let timestamp = std::time::SystemTime::now()
518 .duration_since(std::time::UNIX_EPOCH)
519 .map(|d| d.as_nanos())
520 .unwrap_or(0);
521 temp_dir.join(format!(
522 "{}-{}-{}.tmp",
523 file_name.to_string_lossy(),
524 std::process::id(),
525 timestamp
526 ))
527 }
528
529 fn search_file(
530 &self,
531 path: &Path,
532 pattern: &str,
533 opts: &crate::model::filesystem::FileSearchOptions,
534 cursor: &mut crate::model::filesystem::FileSearchCursor,
535 ) -> io::Result<Vec<crate::model::filesystem::SearchMatch>> {
536 if cursor.done {
537 return Ok(vec![]);
538 }
539
540 let path_str = path.to_string_lossy();
541 let mut params = serde_json::json!({
542 "path": path_str,
543 "pattern": pattern,
544 "fixed_string": opts.fixed_string,
545 "case_sensitive": opts.case_sensitive,
546 "whole_word": opts.whole_word,
547 "max_matches": opts.max_matches,
548 "offset": cursor.offset,
549 "running_line": cursor.running_line,
550 });
551 if let Some(end) = cursor.end_offset {
552 params["end_offset"] = serde_json::json!(end);
553 }
554
555 let result = self
556 .channel
557 .request_blocking("search_file", params)
558 .map_err(Self::to_io_error)?;
559
560 cursor.offset = result
561 .get("next_offset")
562 .and_then(|v| v.as_u64())
563 .unwrap_or(0) as usize;
564 cursor.running_line = result
565 .get("running_line")
566 .and_then(|v| v.as_u64())
567 .unwrap_or(1) as usize;
568 cursor.done = result.get("done").and_then(|v| v.as_bool()).unwrap_or(true);
569
570 let matches: Vec<crate::model::filesystem::SearchMatch> = result
571 .get("matches")
572 .and_then(|v| v.as_array())
573 .map(|arr| {
574 arr.iter()
575 .filter_map(|m| {
576 Some(crate::model::filesystem::SearchMatch {
577 byte_offset: m.get("byte_offset")?.as_u64()? as usize,
578 length: m.get("length")?.as_u64()? as usize,
579 line: m.get("line")?.as_u64()? as usize,
580 column: m.get("column")?.as_u64()? as usize,
581 context: m.get("context")?.as_str()?.to_string(),
582 })
583 })
584 .collect()
585 })
586 .unwrap_or_default();
587
588 Ok(matches)
589 }
590
591 fn sudo_write(
592 &self,
593 path: &Path,
594 data: &[u8],
595 mode: u32,
596 uid: u32,
597 gid: u32,
598 ) -> io::Result<()> {
599 let path_str = path.to_string_lossy();
600 self.channel
601 .request_blocking(
602 "sudo_write",
603 sudo_write_params(&path_str, data, mode, uid, gid),
604 )
605 .map_err(Self::to_io_error)?;
606 Ok(())
607 }
608
609 fn walk_files(
610 &self,
611 root: &Path,
612 skip_dirs: &[&str],
613 cancel: &std::sync::atomic::AtomicBool,
614 on_file: &mut dyn FnMut(&Path, &str) -> bool,
615 ) -> io::Result<()> {
616 let path_str = root.to_string_lossy();
617 let params = serde_json::json!({
618 "path": path_str,
619 "skip_dirs": skip_dirs,
620 });
621
622 let (mut data_rx, result_rx) = self
626 .channel
627 .request_streaming_blocking("walk_files", params)
628 .map_err(Self::to_io_error)?;
629
630 while let Some(data) = data_rx.blocking_recv() {
632 if cancel.load(std::sync::atomic::Ordering::Relaxed) {
633 drop(data_rx);
635 drop(result_rx);
636 return Ok(());
637 }
638
639 if let Some(files) = data.get("files").and_then(|v| v.as_array()) {
640 for file in files {
641 if cancel.load(std::sync::atomic::Ordering::Relaxed) {
642 drop(result_rx);
643 return Ok(());
644 }
645 if let Some(rel) = file.as_str() {
646 let abs = root.join(rel);
647 if !on_file(&abs, rel) {
648 drop(result_rx);
651 return Ok(());
652 }
653 }
654 }
655 }
656 }
657
658 drop(result_rx.blocking_recv());
661 Ok(())
662 }
663}
664
665struct RemoteFileReader {
667 cursor: Cursor<Vec<u8>>,
668}
669
670impl RemoteFileReader {
671 fn new(data: Vec<u8>) -> Self {
672 Self {
673 cursor: Cursor::new(data),
674 }
675 }
676}
677
678impl Read for RemoteFileReader {
679 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
680 self.cursor.read(buf)
681 }
682}
683
684impl Seek for RemoteFileReader {
685 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
686 self.cursor.seek(pos)
687 }
688}
689
690impl FileReader for RemoteFileReader {}
691
692struct RemoteFileWriter {
694 channel: Arc<AgentChannel>,
695 path: PathBuf,
696 buffer: Vec<u8>,
697}
698
699impl RemoteFileWriter {
700 fn new(channel: Arc<AgentChannel>, path: PathBuf) -> Self {
701 Self {
702 channel,
703 path,
704 buffer: Vec::new(),
705 }
706 }
707}
708
709impl Write for RemoteFileWriter {
710 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
711 self.buffer.extend_from_slice(buf);
712 Ok(buf.len())
713 }
714
715 fn flush(&mut self) -> io::Result<()> {
716 Ok(())
718 }
719}
720
721impl FileWriter for RemoteFileWriter {
722 fn sync_all(&self) -> io::Result<()> {
723 let path_str = self.path.to_string_lossy();
724 self.channel
725 .request_blocking("write", write_params(&path_str, &self.buffer))
726 .map_err(RemoteFileSystem::to_io_error)?;
727 Ok(())
728 }
729}
730
731struct AppendingRemoteFileWriter {
733 channel: Arc<AgentChannel>,
734 path: PathBuf,
735 buffer: Vec<u8>,
736}
737
738impl AppendingRemoteFileWriter {
739 fn new(channel: Arc<AgentChannel>, path: PathBuf) -> Self {
740 Self {
741 channel,
742 path,
743 buffer: Vec::new(),
744 }
745 }
746}
747
748impl Write for AppendingRemoteFileWriter {
749 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
750 self.buffer.extend_from_slice(buf);
751 Ok(buf.len())
752 }
753
754 fn flush(&mut self) -> io::Result<()> {
755 Ok(())
756 }
757}
758
759impl FileWriter for AppendingRemoteFileWriter {
760 fn sync_all(&self) -> io::Result<()> {
761 if self.buffer.is_empty() {
762 return Ok(());
763 }
764 let path_str = self.path.to_string_lossy();
765 self.channel
766 .request_blocking("append", append_params(&path_str, &self.buffer))
767 .map_err(RemoteFileSystem::to_io_error)?;
768 Ok(())
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 #[test]
777 fn test_convert_metadata() {
778 #[cfg(unix)]
781 let (uid, gid) = {
782 let (euid, groups) = crate::model::filesystem::StdFileSystem::current_user_groups();
783 (euid, *groups.first().unwrap_or(&0u32))
784 };
785 #[cfg(not(unix))]
786 let (uid, gid) = (1000u32, 1000u32);
787
788 let rm = RemoteMetadata {
789 size: 1234,
790 mtime: 1700000000,
791 mode: 0o644,
792 uid,
793 gid,
794 dir: false,
795 file: true,
796 link: false,
797 };
798
799 let meta = RemoteFileSystem::convert_metadata(&rm, "test.txt");
800 assert_eq!(meta.size, 1234);
801 assert!(!meta.is_hidden);
802 assert!(!meta.is_readonly);
803
804 let meta = RemoteFileSystem::convert_metadata(&rm, ".hidden");
805 assert!(meta.is_hidden);
806 }
807
808 #[test]
809 fn test_convert_dir_entry() {
810 let re = RemoteDirEntry {
811 name: "file.rs".to_string(),
812 path: "/home/user/file.rs".to_string(),
813 dir: false,
814 file: true,
815 link: false,
816 link_dir: false,
817 size: 100,
818 mtime: 1700000000,
819 mode: 0o644,
820 };
821
822 let entry = RemoteFileSystem::convert_dir_entry(&re);
823 assert_eq!(entry.name, "file.rs");
824 assert_eq!(entry.entry_type, EntryType::File);
825 assert!(!entry.is_symlink());
826 }
827}