1use std::io::{self, Read, Seek, Write};
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum EntryType {
23 File,
24 Directory,
25 Symlink,
26}
27
28#[derive(Debug, Clone)]
30pub struct DirEntry {
31 pub path: PathBuf,
33 pub name: String,
35 pub entry_type: EntryType,
37 pub metadata: Option<FileMetadata>,
39 pub symlink_target_is_dir: bool,
41}
42
43impl DirEntry {
44 pub fn new(path: PathBuf, name: String, entry_type: EntryType) -> Self {
46 Self {
47 path,
48 name,
49 entry_type,
50 metadata: None,
51 symlink_target_is_dir: false,
52 }
53 }
54
55 pub fn new_symlink(path: PathBuf, name: String, target_is_dir: bool) -> Self {
57 Self {
58 path,
59 name,
60 entry_type: EntryType::Symlink,
61 metadata: None,
62 symlink_target_is_dir: target_is_dir,
63 }
64 }
65
66 pub fn with_metadata(mut self, metadata: FileMetadata) -> Self {
68 self.metadata = Some(metadata);
69 self
70 }
71
72 pub fn is_dir(&self) -> bool {
74 self.entry_type == EntryType::Directory
75 || (self.entry_type == EntryType::Symlink && self.symlink_target_is_dir)
76 }
77
78 pub fn is_file(&self) -> bool {
80 self.entry_type == EntryType::File
81 || (self.entry_type == EntryType::Symlink && !self.symlink_target_is_dir)
82 }
83
84 pub fn is_symlink(&self) -> bool {
86 self.entry_type == EntryType::Symlink
87 }
88}
89
90#[derive(Debug, Clone)]
96pub struct FileMetadata {
97 pub size: u64,
99 pub modified: Option<SystemTime>,
101 pub permissions: Option<FilePermissions>,
103 pub is_hidden: bool,
105 pub is_readonly: bool,
107 #[cfg(unix)]
109 pub uid: Option<u32>,
110 #[cfg(unix)]
112 pub gid: Option<u32>,
113}
114
115impl FileMetadata {
116 pub fn new(size: u64) -> Self {
118 Self {
119 size,
120 modified: None,
121 permissions: None,
122 is_hidden: false,
123 is_readonly: false,
124 #[cfg(unix)]
125 uid: None,
126 #[cfg(unix)]
127 gid: None,
128 }
129 }
130
131 pub fn with_modified(mut self, modified: SystemTime) -> Self {
133 self.modified = Some(modified);
134 self
135 }
136
137 pub fn with_hidden(mut self, hidden: bool) -> Self {
139 self.is_hidden = hidden;
140 self
141 }
142
143 pub fn with_readonly(mut self, readonly: bool) -> Self {
145 self.is_readonly = readonly;
146 self
147 }
148
149 pub fn with_permissions(mut self, permissions: FilePermissions) -> Self {
151 self.permissions = Some(permissions);
152 self
153 }
154}
155
156impl Default for FileMetadata {
157 fn default() -> Self {
158 Self::new(0)
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct FilePermissions {
165 #[cfg(unix)]
166 mode: u32,
167 #[cfg(not(unix))]
168 readonly: bool,
169}
170
171impl FilePermissions {
172 #[cfg(unix)]
174 pub fn from_mode(mode: u32) -> Self {
175 Self { mode }
176 }
177
178 #[cfg(not(unix))]
180 pub fn from_mode(mode: u32) -> Self {
181 Self {
182 readonly: mode & 0o222 == 0,
183 }
184 }
185
186 #[cfg(unix)]
188 pub fn from_std(perms: std::fs::Permissions) -> Self {
189 use std::os::unix::fs::PermissionsExt;
190 Self { mode: perms.mode() }
191 }
192
193 #[cfg(not(unix))]
194 pub fn from_std(perms: std::fs::Permissions) -> Self {
195 Self {
196 readonly: perms.readonly(),
197 }
198 }
199
200 #[cfg(unix)]
202 pub fn to_std(&self) -> std::fs::Permissions {
203 use std::os::unix::fs::PermissionsExt;
204 std::fs::Permissions::from_mode(self.mode)
205 }
206
207 #[cfg(not(unix))]
208 pub fn to_std(&self) -> std::fs::Permissions {
209 let mut perms = std::fs::Permissions::from(std::fs::metadata(".").unwrap().permissions());
210 perms.set_readonly(self.readonly);
211 perms
212 }
213
214 #[cfg(unix)]
216 pub fn mode(&self) -> u32 {
217 self.mode
218 }
219
220 pub fn is_readonly(&self) -> bool {
226 #[cfg(unix)]
227 {
228 self.mode & 0o222 == 0
229 }
230 #[cfg(not(unix))]
231 {
232 self.readonly
233 }
234 }
235
236 #[cfg(unix)]
241 pub fn is_readonly_for_user(
242 &self,
243 user_uid: u32,
244 file_uid: u32,
245 file_gid: u32,
246 user_groups: &[u32],
247 ) -> bool {
248 if user_uid == 0 {
250 return false;
251 }
252 if user_uid == file_uid {
253 return self.mode & 0o200 == 0;
254 }
255 if user_groups.contains(&file_gid) {
256 return self.mode & 0o020 == 0;
257 }
258 self.mode & 0o002 == 0
259 }
260}
261
262pub trait FileWriter: Write + Send {
268 fn sync_all(&self) -> io::Result<()>;
270}
271
272#[derive(Debug, Clone)]
278pub enum WriteOp<'a> {
279 Copy { offset: u64, len: u64 },
281 Insert { data: &'a [u8] },
283}
284
285struct StdFileWriter(std::fs::File);
287
288impl Write for StdFileWriter {
289 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
290 self.0.write(buf)
291 }
292
293 fn flush(&mut self) -> io::Result<()> {
294 self.0.flush()
295 }
296}
297
298impl FileWriter for StdFileWriter {
299 fn sync_all(&self) -> io::Result<()> {
300 self.0.sync_all()
301 }
302}
303
304pub trait FileReader: Read + Seek + Send {}
306
307struct StdFileReader(std::fs::File);
309
310impl Read for StdFileReader {
311 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
312 self.0.read(buf)
313 }
314}
315
316impl Seek for StdFileReader {
317 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
318 self.0.seek(pos)
319 }
320}
321
322impl FileReader for StdFileReader {}
323
324#[derive(Clone, Debug)]
339pub struct FileSearchOptions {
340 pub fixed_string: bool,
342 pub case_sensitive: bool,
344 pub whole_word: bool,
346 pub max_matches: usize,
348}
349
350#[derive(Clone, Debug)]
353pub struct FileSearchCursor {
354 pub offset: usize,
356 pub running_line: usize,
358 pub done: bool,
360 pub end_offset: Option<usize>,
364}
365
366impl Default for FileSearchCursor {
367 fn default() -> Self {
368 Self {
369 offset: 0,
370 running_line: 1,
371 done: false,
372 end_offset: None,
373 }
374 }
375}
376
377impl FileSearchCursor {
378 pub fn new() -> Self {
379 Self::default()
380 }
381
382 pub fn for_range(offset: usize, end_offset: usize, running_line: usize) -> Self {
384 Self {
385 offset,
386 running_line,
387 done: false,
388 end_offset: Some(end_offset),
389 }
390 }
391}
392
393#[derive(Clone, Debug)]
398pub struct SearchMatch {
399 pub byte_offset: usize,
401 pub length: usize,
403 pub line: usize,
405 pub column: usize,
407 pub context: String,
409}
410
411pub trait FileSystem: Send + Sync {
425 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>>;
431
432 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>>;
434
435 fn count_line_feeds_in_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<usize> {
443 let data = self.read_range(path, offset, len)?;
444 Ok(data.iter().filter(|&&b| b == b'\n').count())
445 }
446
447 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()>;
449
450 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
452
453 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>>;
455
456 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
458
459 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
461
462 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()>;
464
465 fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
480 let mut buffer = Vec::new();
482 for op in ops {
483 match op {
484 WriteOp::Copy { offset, len } => {
485 let data = self.read_range(src_path, *offset, *len as usize)?;
486 buffer.extend_from_slice(&data);
487 }
488 WriteOp::Insert { data } => {
489 buffer.extend_from_slice(data);
490 }
491 }
492 }
493 self.write_file(dst_path, &buffer)
494 }
495
496 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
502
503 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64>;
505
506 fn remove_file(&self, path: &Path) -> io::Result<()>;
508
509 fn remove_dir(&self, path: &Path) -> io::Result<()>;
511
512 fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
518
519 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
521
522 fn exists(&self, path: &Path) -> bool {
524 self.metadata(path).is_ok()
525 }
526
527 fn metadata_if_exists(&self, path: &Path) -> Option<FileMetadata> {
529 self.metadata(path).ok()
530 }
531
532 fn is_dir(&self, path: &Path) -> io::Result<bool>;
534
535 fn is_file(&self, path: &Path) -> io::Result<bool>;
537
538 fn is_writable(&self, path: &Path) -> bool {
546 self.metadata(path).map(|m| !m.is_readonly).unwrap_or(false)
547 }
548
549 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()>;
551
552 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
558
559 fn create_dir(&self, path: &Path) -> io::Result<()>;
561
562 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
564
565 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
571
572 fn current_uid(&self) -> u32;
578
579 fn is_owner(&self, path: &Path) -> bool {
581 #[cfg(unix)]
582 {
583 if let Ok(meta) = self.metadata(path) {
584 if let Some(uid) = meta.uid {
585 return uid == self.current_uid();
586 }
587 }
588 true
589 }
590 #[cfg(not(unix))]
591 {
592 let _ = path;
593 true
594 }
595 }
596
597 fn temp_path_for(&self, path: &Path) -> PathBuf {
599 path.with_extension("tmp")
600 }
601
602 fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
604 let temp_dir = std::env::temp_dir();
605 let file_name = dest_path
606 .file_name()
607 .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
608 let timestamp = std::time::SystemTime::now()
609 .duration_since(std::time::UNIX_EPOCH)
610 .map(|d| d.as_nanos())
611 .unwrap_or(0);
612 temp_dir.join(format!(
613 "{}-{}-{}.tmp",
614 file_name.to_string_lossy(),
615 std::process::id(),
616 timestamp
617 ))
618 }
619
620 fn remote_connection_info(&self) -> Option<&str> {
629 None
630 }
631
632 fn is_remote_connected(&self) -> bool {
638 true
639 }
640
641 fn home_dir(&self) -> io::Result<PathBuf> {
646 dirs::home_dir()
647 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))
648 }
649
650 fn search_file(
667 &self,
668 path: &Path,
669 pattern: &str,
670 opts: &FileSearchOptions,
671 cursor: &mut FileSearchCursor,
672 ) -> io::Result<Vec<SearchMatch>>;
673
674 fn sudo_write(&self, path: &Path, data: &[u8], mode: u32, uid: u32, gid: u32)
685 -> io::Result<()>;
686}
687
688pub trait FileSystemExt: FileSystem {
713 fn read_file_async(
715 &self,
716 path: &Path,
717 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
718 async { self.read_file(path) }
719 }
720
721 fn read_range_async(
723 &self,
724 path: &Path,
725 offset: u64,
726 len: usize,
727 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
728 async move { self.read_range(path, offset, len) }
729 }
730
731 fn count_line_feeds_in_range_async(
733 &self,
734 path: &Path,
735 offset: u64,
736 len: usize,
737 ) -> impl std::future::Future<Output = io::Result<usize>> + Send {
738 async move { self.count_line_feeds_in_range(path, offset, len) }
739 }
740
741 fn write_file_async(
743 &self,
744 path: &Path,
745 data: &[u8],
746 ) -> impl std::future::Future<Output = io::Result<()>> + Send {
747 async { self.write_file(path, data) }
748 }
749
750 fn metadata_async(
752 &self,
753 path: &Path,
754 ) -> impl std::future::Future<Output = io::Result<FileMetadata>> + Send {
755 async { self.metadata(path) }
756 }
757
758 fn exists_async(&self, path: &Path) -> impl std::future::Future<Output = bool> + Send {
760 async { self.exists(path) }
761 }
762
763 fn is_dir_async(
765 &self,
766 path: &Path,
767 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
768 async { self.is_dir(path) }
769 }
770
771 fn is_file_async(
773 &self,
774 path: &Path,
775 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
776 async { self.is_file(path) }
777 }
778
779 fn read_dir_async(
781 &self,
782 path: &Path,
783 ) -> impl std::future::Future<Output = io::Result<Vec<DirEntry>>> + Send {
784 async { self.read_dir(path) }
785 }
786
787 fn canonicalize_async(
789 &self,
790 path: &Path,
791 ) -> impl std::future::Future<Output = io::Result<PathBuf>> + Send {
792 async { self.canonicalize(path) }
793 }
794}
795
796impl<T: FileSystem> FileSystemExt for T {}
798
799pub fn build_search_regex(
805 pattern: &str,
806 opts: &FileSearchOptions,
807) -> io::Result<regex::bytes::Regex> {
808 let re_pattern = if opts.fixed_string {
809 regex::escape(pattern)
810 } else {
811 pattern.to_string()
812 };
813 let re_pattern = if opts.whole_word {
814 format!(r"\b{}\b", re_pattern)
815 } else {
816 re_pattern
817 };
818 let re_pattern = if opts.case_sensitive {
819 re_pattern
820 } else {
821 format!("(?i){}", re_pattern)
822 };
823 regex::bytes::Regex::new(&re_pattern)
824 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))
825}
826
827pub fn default_search_file(
831 fs: &dyn FileSystem,
832 path: &Path,
833 pattern: &str,
834 opts: &FileSearchOptions,
835 cursor: &mut FileSearchCursor,
836) -> io::Result<Vec<SearchMatch>> {
837 if cursor.done {
838 return Ok(vec![]);
839 }
840
841 const CHUNK_SIZE: usize = 1_048_576; let overlap = pattern.len().max(256);
843
844 let file_len = fs.metadata(path)?.size as usize;
845 let effective_end = cursor.end_offset.unwrap_or(file_len).min(file_len);
846
847 if cursor.offset == 0 && cursor.end_offset.is_none() {
849 if file_len == 0 {
850 cursor.done = true;
851 return Ok(vec![]);
852 }
853 let header_len = file_len.min(8192);
854 let header = fs.read_range(path, 0, header_len)?;
855 if header.contains(&0) {
856 cursor.done = true;
857 return Ok(vec![]);
858 }
859 }
860
861 if cursor.offset >= effective_end {
862 cursor.done = true;
863 return Ok(vec![]);
864 }
865
866 let regex = build_search_regex(pattern, opts)?;
867
868 let read_start = cursor.offset.saturating_sub(overlap);
870 let read_end = (read_start + CHUNK_SIZE).min(effective_end);
871 let chunk = fs.read_range(path, read_start as u64, read_end - read_start)?;
872
873 let overlap_len = cursor.offset - read_start;
874
875 let newlines_in_overlap = chunk[..overlap_len].iter().filter(|&&b| b == b'\n').count();
877 let mut line_at = cursor.running_line.saturating_sub(newlines_in_overlap);
878 let mut counted_to = 0usize;
879 let mut matches = Vec::new();
880
881 for m in regex.find_iter(&chunk) {
882 if overlap_len > 0 && m.end() <= overlap_len {
884 continue;
885 }
886 if matches.len() >= opts.max_matches {
887 break;
888 }
889
890 line_at += chunk[counted_to..m.start()]
892 .iter()
893 .filter(|&&b| b == b'\n')
894 .count();
895 counted_to = m.start();
896
897 let line_start = chunk[..m.start()]
899 .iter()
900 .rposition(|&b| b == b'\n')
901 .map(|p| p + 1)
902 .unwrap_or(0);
903 let line_end = chunk[m.start()..]
904 .iter()
905 .position(|&b| b == b'\n')
906 .map(|p| m.start() + p)
907 .unwrap_or(chunk.len());
908
909 let column = m.start() - line_start + 1;
910 let context = String::from_utf8_lossy(&chunk[line_start..line_end]).into_owned();
911
912 matches.push(SearchMatch {
913 byte_offset: read_start + m.start(),
914 length: m.end() - m.start(),
915 line: line_at,
916 column,
917 context,
918 });
919 }
920
921 let new_data = &chunk[overlap_len..];
923 cursor.running_line += new_data.iter().filter(|&&b| b == b'\n').count();
924 cursor.offset = read_end;
925 if read_end >= effective_end {
926 cursor.done = true;
927 }
928
929 Ok(matches)
930}
931
932#[derive(Debug, Clone, Copy, Default)]
940pub struct StdFileSystem;
941
942impl StdFileSystem {
943 fn is_hidden(path: &Path) -> bool {
945 path.file_name()
946 .and_then(|n| n.to_str())
947 .is_some_and(|n| n.starts_with('.'))
948 }
949
950 #[cfg(unix)]
952 pub fn current_user_groups() -> (u32, Vec<u32>) {
953 let euid = unsafe { libc::geteuid() };
955 let egid = unsafe { libc::getegid() };
956 let mut groups = vec![egid];
957
958 let ngroups = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
960 if ngroups > 0 {
961 let mut sup_groups = vec![0 as libc::gid_t; ngroups as usize];
962 let n = unsafe { libc::getgroups(ngroups, sup_groups.as_mut_ptr()) };
963 if n > 0 {
964 sup_groups.truncate(n as usize);
965 for g in sup_groups {
966 if g != egid {
967 groups.push(g);
968 }
969 }
970 }
971 }
972
973 (euid, groups)
974 }
975
976 fn build_metadata(path: &Path, meta: &std::fs::Metadata) -> FileMetadata {
978 #[cfg(unix)]
979 {
980 use std::os::unix::fs::MetadataExt;
981 let file_uid = meta.uid();
982 let file_gid = meta.gid();
983 let permissions = FilePermissions::from_std(meta.permissions());
984 let (euid, user_groups) = Self::current_user_groups();
985 let is_readonly =
986 permissions.is_readonly_for_user(euid, file_uid, file_gid, &user_groups);
987 FileMetadata {
988 size: meta.len(),
989 modified: meta.modified().ok(),
990 permissions: Some(permissions),
991 is_hidden: Self::is_hidden(path),
992 is_readonly,
993 uid: Some(file_uid),
994 gid: Some(file_gid),
995 }
996 }
997 #[cfg(not(unix))]
998 {
999 FileMetadata {
1000 size: meta.len(),
1001 modified: meta.modified().ok(),
1002 permissions: Some(FilePermissions::from_std(meta.permissions())),
1003 is_hidden: Self::is_hidden(path),
1004 is_readonly: meta.permissions().readonly(),
1005 }
1006 }
1007 }
1008}
1009
1010impl FileSystem for StdFileSystem {
1011 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
1013 let data = std::fs::read(path)?;
1014 crate::services::counters::global().inc_disk_bytes_read(data.len() as u64);
1015 Ok(data)
1016 }
1017
1018 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
1019 let mut file = std::fs::File::open(path)?;
1020 file.seek(io::SeekFrom::Start(offset))?;
1021 let mut buffer = vec![0u8; len];
1022 file.read_exact(&mut buffer)?;
1023 crate::services::counters::global().inc_disk_bytes_read(len as u64);
1024 Ok(buffer)
1025 }
1026
1027 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1028 let original_metadata = self.metadata_if_exists(path);
1029 let temp_path = self.temp_path_for(path);
1030 {
1031 let mut file = self.create_file(&temp_path)?;
1032 file.write_all(data)?;
1033 file.sync_all()?;
1034 }
1035 if let Some(ref meta) = original_metadata {
1036 if let Some(ref perms) = meta.permissions {
1037 #[allow(clippy::let_underscore_must_use)]
1039 let _ = self.set_permissions(&temp_path, perms);
1040 }
1041 }
1042 self.rename(&temp_path, path)?;
1043 Ok(())
1044 }
1045
1046 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
1047 let file = std::fs::File::create(path)?;
1048 Ok(Box::new(StdFileWriter(file)))
1049 }
1050
1051 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
1052 let file = std::fs::File::open(path)?;
1053 Ok(Box::new(StdFileReader(file)))
1054 }
1055
1056 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
1057 let file = std::fs::OpenOptions::new()
1058 .write(true)
1059 .truncate(true)
1060 .open(path)?;
1061 Ok(Box::new(StdFileWriter(file)))
1062 }
1063
1064 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
1065 let file = std::fs::OpenOptions::new()
1066 .create(true)
1067 .append(true)
1068 .open(path)?;
1069 Ok(Box::new(StdFileWriter(file)))
1070 }
1071
1072 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
1073 let file = std::fs::OpenOptions::new().write(true).open(path)?;
1074 file.set_len(len)
1075 }
1076
1077 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1079 std::fs::rename(from, to)
1080 }
1081
1082 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
1083 std::fs::copy(from, to)
1084 }
1085
1086 fn remove_file(&self, path: &Path) -> io::Result<()> {
1087 std::fs::remove_file(path)
1088 }
1089
1090 fn remove_dir(&self, path: &Path) -> io::Result<()> {
1091 std::fs::remove_dir(path)
1092 }
1093
1094 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
1096 let meta = std::fs::metadata(path)?;
1097 Ok(Self::build_metadata(path, &meta))
1098 }
1099
1100 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
1101 let meta = std::fs::symlink_metadata(path)?;
1102 Ok(Self::build_metadata(path, &meta))
1103 }
1104
1105 fn is_dir(&self, path: &Path) -> io::Result<bool> {
1106 Ok(std::fs::metadata(path)?.is_dir())
1107 }
1108
1109 fn is_file(&self, path: &Path) -> io::Result<bool> {
1110 Ok(std::fs::metadata(path)?.is_file())
1111 }
1112
1113 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
1114 std::fs::set_permissions(path, permissions.to_std())
1115 }
1116
1117 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
1119 let mut entries = Vec::new();
1120 for entry in std::fs::read_dir(path)? {
1121 let entry = entry?;
1122 let path = entry.path();
1123 let name = entry.file_name().to_string_lossy().into_owned();
1124 let file_type = entry.file_type()?;
1125
1126 let entry_type = if file_type.is_dir() {
1127 EntryType::Directory
1128 } else if file_type.is_symlink() {
1129 EntryType::Symlink
1130 } else {
1131 EntryType::File
1132 };
1133
1134 let mut dir_entry = DirEntry::new(path.clone(), name, entry_type);
1135
1136 if file_type.is_symlink() {
1138 dir_entry.symlink_target_is_dir = std::fs::metadata(&path)
1139 .map(|m| m.is_dir())
1140 .unwrap_or(false);
1141 }
1142
1143 entries.push(dir_entry);
1144 }
1145 Ok(entries)
1146 }
1147
1148 fn create_dir(&self, path: &Path) -> io::Result<()> {
1149 std::fs::create_dir(path)
1150 }
1151
1152 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
1153 std::fs::create_dir_all(path)
1154 }
1155
1156 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
1158 std::fs::canonicalize(path)
1159 }
1160
1161 fn current_uid(&self) -> u32 {
1163 #[cfg(all(unix, feature = "runtime"))]
1164 {
1165 unsafe { libc::getuid() }
1167 }
1168 #[cfg(not(all(unix, feature = "runtime")))]
1169 {
1170 0
1171 }
1172 }
1173
1174 fn sudo_write(
1175 &self,
1176 path: &Path,
1177 data: &[u8],
1178 mode: u32,
1179 uid: u32,
1180 gid: u32,
1181 ) -> io::Result<()> {
1182 use std::process::{Command, Stdio};
1183
1184 let mut child = Command::new("sudo")
1186 .args(["tee", &path.to_string_lossy()])
1187 .stdin(Stdio::piped())
1188 .stdout(Stdio::null())
1189 .stderr(Stdio::piped())
1190 .spawn()
1191 .map_err(|e| io::Error::other(format!("failed to spawn sudo: {}", e)))?;
1192
1193 if let Some(mut stdin) = child.stdin.take() {
1194 use std::io::Write;
1195 stdin.write_all(data)?;
1196 }
1197
1198 let output = child.wait_with_output()?;
1199 if !output.status.success() {
1200 let stderr = String::from_utf8_lossy(&output.stderr);
1201 return Err(io::Error::new(
1202 io::ErrorKind::PermissionDenied,
1203 format!("sudo tee failed: {}", stderr.trim()),
1204 ));
1205 }
1206
1207 let status = Command::new("sudo")
1209 .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()])
1210 .status()?;
1211 if !status.success() {
1212 return Err(io::Error::other("sudo chmod failed"));
1213 }
1214
1215 let status = Command::new("sudo")
1217 .args([
1218 "chown",
1219 &format!("{}:{}", uid, gid),
1220 &path.to_string_lossy(),
1221 ])
1222 .status()?;
1223 if !status.success() {
1224 return Err(io::Error::other("sudo chown failed"));
1225 }
1226
1227 Ok(())
1228 }
1229
1230 fn search_file(
1231 &self,
1232 path: &Path,
1233 pattern: &str,
1234 opts: &FileSearchOptions,
1235 cursor: &mut FileSearchCursor,
1236 ) -> io::Result<Vec<SearchMatch>> {
1237 default_search_file(self, path, pattern, opts, cursor)
1238 }
1239}
1240
1241#[derive(Debug, Clone, Copy, Default)]
1250pub struct NoopFileSystem;
1251
1252impl NoopFileSystem {
1253 fn unsupported<T>() -> io::Result<T> {
1254 Err(io::Error::new(
1255 io::ErrorKind::Unsupported,
1256 "Filesystem not available",
1257 ))
1258 }
1259}
1260
1261impl FileSystem for NoopFileSystem {
1262 fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
1263 Self::unsupported()
1264 }
1265
1266 fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> io::Result<Vec<u8>> {
1267 Self::unsupported()
1268 }
1269
1270 fn write_file(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1271 Self::unsupported()
1272 }
1273
1274 fn create_file(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
1275 Self::unsupported()
1276 }
1277
1278 fn open_file(&self, _path: &Path) -> io::Result<Box<dyn FileReader>> {
1279 Self::unsupported()
1280 }
1281
1282 fn open_file_for_write(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
1283 Self::unsupported()
1284 }
1285
1286 fn open_file_for_append(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
1287 Self::unsupported()
1288 }
1289
1290 fn set_file_length(&self, _path: &Path, _len: u64) -> io::Result<()> {
1291 Self::unsupported()
1292 }
1293
1294 fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1295 Self::unsupported()
1296 }
1297
1298 fn copy(&self, _from: &Path, _to: &Path) -> io::Result<u64> {
1299 Self::unsupported()
1300 }
1301
1302 fn remove_file(&self, _path: &Path) -> io::Result<()> {
1303 Self::unsupported()
1304 }
1305
1306 fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1307 Self::unsupported()
1308 }
1309
1310 fn metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
1311 Self::unsupported()
1312 }
1313
1314 fn symlink_metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
1315 Self::unsupported()
1316 }
1317
1318 fn is_dir(&self, _path: &Path) -> io::Result<bool> {
1319 Self::unsupported()
1320 }
1321
1322 fn is_file(&self, _path: &Path) -> io::Result<bool> {
1323 Self::unsupported()
1324 }
1325
1326 fn set_permissions(&self, _path: &Path, _permissions: &FilePermissions) -> io::Result<()> {
1327 Self::unsupported()
1328 }
1329
1330 fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1331 Self::unsupported()
1332 }
1333
1334 fn create_dir(&self, _path: &Path) -> io::Result<()> {
1335 Self::unsupported()
1336 }
1337
1338 fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1339 Self::unsupported()
1340 }
1341
1342 fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1343 Self::unsupported()
1344 }
1345
1346 fn current_uid(&self) -> u32 {
1347 0
1348 }
1349
1350 fn search_file(
1351 &self,
1352 _path: &Path,
1353 _pattern: &str,
1354 _opts: &FileSearchOptions,
1355 _cursor: &mut FileSearchCursor,
1356 ) -> io::Result<Vec<SearchMatch>> {
1357 Self::unsupported()
1358 }
1359
1360 fn sudo_write(
1361 &self,
1362 _path: &Path,
1363 _data: &[u8],
1364 _mode: u32,
1365 _uid: u32,
1366 _gid: u32,
1367 ) -> io::Result<()> {
1368 Self::unsupported()
1369 }
1370}
1371
1372#[cfg(test)]
1377mod tests {
1378 use super::*;
1379 use tempfile::NamedTempFile;
1380
1381 #[test]
1382 fn test_std_filesystem_read_write() {
1383 let fs = StdFileSystem;
1384 let mut temp = NamedTempFile::new().unwrap();
1385 let path = temp.path().to_path_buf();
1386
1387 std::io::Write::write_all(&mut temp, b"Hello, World!").unwrap();
1388 std::io::Write::flush(&mut temp).unwrap();
1389
1390 let content = fs.read_file(&path).unwrap();
1391 assert_eq!(content, b"Hello, World!");
1392
1393 let range = fs.read_range(&path, 7, 5).unwrap();
1394 assert_eq!(range, b"World");
1395
1396 let meta = fs.metadata(&path).unwrap();
1397 assert_eq!(meta.size, 13);
1398 }
1399
1400 #[test]
1401 fn test_noop_filesystem() {
1402 let fs = NoopFileSystem;
1403 let path = Path::new("/some/path");
1404
1405 assert!(fs.read_file(path).is_err());
1406 assert!(fs.read_range(path, 0, 10).is_err());
1407 assert!(fs.write_file(path, b"data").is_err());
1408 assert!(fs.metadata(path).is_err());
1409 assert!(fs.read_dir(path).is_err());
1410 }
1411
1412 #[test]
1413 fn test_create_and_write_file() {
1414 let fs = StdFileSystem;
1415 let temp_dir = tempfile::tempdir().unwrap();
1416 let path = temp_dir.path().join("test.txt");
1417
1418 {
1419 let mut writer = fs.create_file(&path).unwrap();
1420 writer.write_all(b"test content").unwrap();
1421 writer.sync_all().unwrap();
1422 }
1423
1424 let content = fs.read_file(&path).unwrap();
1425 assert_eq!(content, b"test content");
1426 }
1427
1428 #[test]
1429 fn test_read_dir() {
1430 let fs = StdFileSystem;
1431 let temp_dir = tempfile::tempdir().unwrap();
1432
1433 fs.create_dir(&temp_dir.path().join("subdir")).unwrap();
1435 fs.write_file(&temp_dir.path().join("file1.txt"), b"content1")
1436 .unwrap();
1437 fs.write_file(&temp_dir.path().join("file2.txt"), b"content2")
1438 .unwrap();
1439
1440 let entries = fs.read_dir(temp_dir.path()).unwrap();
1441 assert_eq!(entries.len(), 3);
1442
1443 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1444 assert!(names.contains(&"subdir"));
1445 assert!(names.contains(&"file1.txt"));
1446 assert!(names.contains(&"file2.txt"));
1447 }
1448
1449 #[test]
1450 fn test_dir_entry_types() {
1451 let file = DirEntry::new(PathBuf::from("/file"), "file".to_string(), EntryType::File);
1452 assert!(file.is_file());
1453 assert!(!file.is_dir());
1454
1455 let dir = DirEntry::new(
1456 PathBuf::from("/dir"),
1457 "dir".to_string(),
1458 EntryType::Directory,
1459 );
1460 assert!(dir.is_dir());
1461 assert!(!dir.is_file());
1462
1463 let link_to_dir = DirEntry::new_symlink(PathBuf::from("/link"), "link".to_string(), true);
1464 assert!(link_to_dir.is_symlink());
1465 assert!(link_to_dir.is_dir());
1466 }
1467
1468 #[test]
1469 fn test_metadata_builder() {
1470 let meta = FileMetadata::default()
1471 .with_hidden(true)
1472 .with_readonly(true);
1473 assert!(meta.is_hidden);
1474 assert!(meta.is_readonly);
1475 }
1476
1477 #[test]
1478 fn test_atomic_write() {
1479 let fs = StdFileSystem;
1480 let temp_dir = tempfile::tempdir().unwrap();
1481 let path = temp_dir.path().join("atomic_test.txt");
1482
1483 fs.write_file(&path, b"initial").unwrap();
1484 assert_eq!(fs.read_file(&path).unwrap(), b"initial");
1485
1486 fs.write_file(&path, b"updated").unwrap();
1487 assert_eq!(fs.read_file(&path).unwrap(), b"updated");
1488 }
1489
1490 #[test]
1491 fn test_write_patched_default_impl() {
1492 let fs = StdFileSystem;
1494 let temp_dir = tempfile::tempdir().unwrap();
1495 let src_path = temp_dir.path().join("source.txt");
1496 let dst_path = temp_dir.path().join("dest.txt");
1497
1498 fs.write_file(&src_path, b"AAABBBCCC").unwrap();
1500
1501 let ops = vec![
1503 WriteOp::Copy { offset: 0, len: 3 }, WriteOp::Insert { data: b"XXX" }, WriteOp::Copy { offset: 6, len: 3 }, ];
1507
1508 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1509
1510 let result = fs.read_file(&dst_path).unwrap();
1511 assert_eq!(result, b"AAAXXXCCC");
1512 }
1513
1514 #[test]
1515 fn test_write_patched_same_file() {
1516 let fs = StdFileSystem;
1518 let temp_dir = tempfile::tempdir().unwrap();
1519 let path = temp_dir.path().join("file.txt");
1520
1521 fs.write_file(&path, b"Hello World").unwrap();
1523
1524 let ops = vec![
1526 WriteOp::Copy { offset: 0, len: 6 }, WriteOp::Insert { data: b"Rust" }, ];
1529
1530 fs.write_patched(&path, &path, &ops).unwrap();
1531
1532 let result = fs.read_file(&path).unwrap();
1533 assert_eq!(result, b"Hello Rust");
1534 }
1535
1536 #[test]
1537 fn test_write_patched_insert_only() {
1538 let fs = StdFileSystem;
1540 let temp_dir = tempfile::tempdir().unwrap();
1541 let src_path = temp_dir.path().join("empty.txt");
1542 let dst_path = temp_dir.path().join("new.txt");
1543
1544 fs.write_file(&src_path, b"").unwrap();
1546
1547 let ops = vec![WriteOp::Insert {
1548 data: b"All new content",
1549 }];
1550
1551 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1552
1553 let result = fs.read_file(&dst_path).unwrap();
1554 assert_eq!(result, b"All new content");
1555 }
1556
1557 fn make_search_opts(pattern_is_fixed: bool) -> FileSearchOptions {
1562 FileSearchOptions {
1563 fixed_string: pattern_is_fixed,
1564 case_sensitive: true,
1565 whole_word: false,
1566 max_matches: 100,
1567 }
1568 }
1569
1570 #[test]
1571 fn test_search_file_basic() {
1572 let fs = StdFileSystem;
1573 let temp_dir = tempfile::tempdir().unwrap();
1574 let path = temp_dir.path().join("test.txt");
1575 fs.write_file(&path, b"hello world\nfoo bar\nhello again\n")
1576 .unwrap();
1577
1578 let opts = make_search_opts(true);
1579 let mut cursor = FileSearchCursor::new();
1580 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1581
1582 assert!(cursor.done);
1583 assert_eq!(matches.len(), 2);
1584
1585 assert_eq!(matches[0].line, 1);
1586 assert_eq!(matches[0].column, 1);
1587 assert_eq!(matches[0].context, "hello world");
1588
1589 assert_eq!(matches[1].line, 3);
1590 assert_eq!(matches[1].column, 1);
1591 assert_eq!(matches[1].context, "hello again");
1592 }
1593
1594 #[test]
1595 fn test_search_file_no_matches() {
1596 let fs = StdFileSystem;
1597 let temp_dir = tempfile::tempdir().unwrap();
1598 let path = temp_dir.path().join("test.txt");
1599 fs.write_file(&path, b"hello world\n").unwrap();
1600
1601 let opts = make_search_opts(true);
1602 let mut cursor = FileSearchCursor::new();
1603 let matches = fs
1604 .search_file(&path, "NOTFOUND", &opts, &mut cursor)
1605 .unwrap();
1606
1607 assert!(cursor.done);
1608 assert!(matches.is_empty());
1609 }
1610
1611 #[test]
1612 fn test_search_file_case_insensitive() {
1613 let fs = StdFileSystem;
1614 let temp_dir = tempfile::tempdir().unwrap();
1615 let path = temp_dir.path().join("test.txt");
1616 fs.write_file(&path, b"Hello HELLO hello\n").unwrap();
1617
1618 let opts = FileSearchOptions {
1619 fixed_string: true,
1620 case_sensitive: false,
1621 whole_word: false,
1622 max_matches: 100,
1623 };
1624 let mut cursor = FileSearchCursor::new();
1625 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1626
1627 assert_eq!(matches.len(), 3);
1628 }
1629
1630 #[test]
1631 fn test_search_file_whole_word() {
1632 let fs = StdFileSystem;
1633 let temp_dir = tempfile::tempdir().unwrap();
1634 let path = temp_dir.path().join("test.txt");
1635 fs.write_file(&path, b"cat concatenate catalog\n").unwrap();
1636
1637 let opts = FileSearchOptions {
1638 fixed_string: true,
1639 case_sensitive: true,
1640 whole_word: true,
1641 max_matches: 100,
1642 };
1643 let mut cursor = FileSearchCursor::new();
1644 let matches = fs.search_file(&path, "cat", &opts, &mut cursor).unwrap();
1645
1646 assert_eq!(matches.len(), 1);
1647 assert_eq!(matches[0].column, 1);
1648 }
1649
1650 #[test]
1651 fn test_search_file_regex() {
1652 let fs = StdFileSystem;
1653 let temp_dir = tempfile::tempdir().unwrap();
1654 let path = temp_dir.path().join("test.txt");
1655 fs.write_file(&path, b"foo123 bar456 baz\n").unwrap();
1656
1657 let opts = FileSearchOptions {
1658 fixed_string: false,
1659 case_sensitive: true,
1660 whole_word: false,
1661 max_matches: 100,
1662 };
1663 let mut cursor = FileSearchCursor::new();
1664 let matches = fs
1665 .search_file(&path, r"[a-z]+\d+", &opts, &mut cursor)
1666 .unwrap();
1667
1668 assert_eq!(matches.len(), 2);
1669 assert_eq!(matches[0].context, "foo123 bar456 baz");
1670 }
1671
1672 #[test]
1673 fn test_search_file_binary_skipped() {
1674 let fs = StdFileSystem;
1675 let temp_dir = tempfile::tempdir().unwrap();
1676 let path = temp_dir.path().join("binary.dat");
1677 let mut data = b"hello world\n".to_vec();
1678 data.push(0); data.extend_from_slice(b"hello again\n");
1680 fs.write_file(&path, &data).unwrap();
1681
1682 let opts = make_search_opts(true);
1683 let mut cursor = FileSearchCursor::new();
1684 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1685
1686 assert!(cursor.done);
1687 assert!(matches.is_empty());
1688 }
1689
1690 #[test]
1691 fn test_search_file_empty_file() {
1692 let fs = StdFileSystem;
1693 let temp_dir = tempfile::tempdir().unwrap();
1694 let path = temp_dir.path().join("empty.txt");
1695 fs.write_file(&path, b"").unwrap();
1696
1697 let opts = make_search_opts(true);
1698 let mut cursor = FileSearchCursor::new();
1699 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1700
1701 assert!(cursor.done);
1702 assert!(matches.is_empty());
1703 }
1704
1705 #[test]
1706 fn test_search_file_max_matches() {
1707 let fs = StdFileSystem;
1708 let temp_dir = tempfile::tempdir().unwrap();
1709 let path = temp_dir.path().join("test.txt");
1710 fs.write_file(&path, b"aa bb aa cc aa dd aa\n").unwrap();
1711
1712 let opts = FileSearchOptions {
1713 fixed_string: true,
1714 case_sensitive: true,
1715 whole_word: false,
1716 max_matches: 2,
1717 };
1718 let mut cursor = FileSearchCursor::new();
1719 let matches = fs.search_file(&path, "aa", &opts, &mut cursor).unwrap();
1720
1721 assert_eq!(matches.len(), 2);
1722 }
1723
1724 #[test]
1725 fn test_search_file_cursor_multi_chunk() {
1726 let fs = StdFileSystem;
1727 let temp_dir = tempfile::tempdir().unwrap();
1728 let path = temp_dir.path().join("large.txt");
1729
1730 let mut content = Vec::new();
1732 for i in 0..100_000 {
1733 content.extend_from_slice(format!("line {} content here\n", i).as_bytes());
1734 }
1735 fs.write_file(&path, &content).unwrap();
1736
1737 let opts = FileSearchOptions {
1738 fixed_string: true,
1739 case_sensitive: true,
1740 whole_word: false,
1741 max_matches: 1000,
1742 };
1743 let mut cursor = FileSearchCursor::new();
1744 let mut all_matches = Vec::new();
1745
1746 while !cursor.done {
1747 let batch = fs
1748 .search_file(&path, "line 5000", &opts, &mut cursor)
1749 .unwrap();
1750 all_matches.extend(batch);
1751 }
1752
1753 assert_eq!(all_matches.len(), 11);
1756
1757 let first = &all_matches[0];
1759 assert_eq!(first.line, 5001); assert_eq!(first.column, 1);
1761 assert!(first.context.starts_with("line 5000"));
1762 }
1763
1764 #[test]
1765 fn test_search_file_cursor_no_duplicates() {
1766 let fs = StdFileSystem;
1767 let temp_dir = tempfile::tempdir().unwrap();
1768 let path = temp_dir.path().join("large.txt");
1769
1770 let mut content = Vec::new();
1772 for i in 0..100_000 {
1773 content.extend_from_slice(format!("MARKER_{:06}\n", i).as_bytes());
1774 }
1775 fs.write_file(&path, &content).unwrap();
1776
1777 let opts = FileSearchOptions {
1778 fixed_string: true,
1779 case_sensitive: true,
1780 whole_word: false,
1781 max_matches: 200_000,
1782 };
1783 let mut cursor = FileSearchCursor::new();
1784 let mut all_matches = Vec::new();
1785 let mut batches = 0;
1786
1787 while !cursor.done {
1788 let batch = fs
1789 .search_file(&path, "MARKER_", &opts, &mut cursor)
1790 .unwrap();
1791 all_matches.extend(batch);
1792 batches += 1;
1793 }
1794
1795 assert!(batches > 1, "Expected multiple batches, got {}", batches);
1797 assert_eq!(all_matches.len(), 100_000);
1799 let mut offsets: Vec<usize> = all_matches.iter().map(|m| m.byte_offset).collect();
1801 offsets.sort();
1802 offsets.dedup();
1803 assert_eq!(offsets.len(), 100_000);
1804 }
1805
1806 #[test]
1807 fn test_search_file_line_numbers_across_chunks() {
1808 let fs = StdFileSystem;
1809 let temp_dir = tempfile::tempdir().unwrap();
1810 let path = temp_dir.path().join("large.txt");
1811
1812 let mut content = Vec::new();
1814 let total_lines = 100_000;
1815 for i in 0..total_lines {
1816 if i == 99_999 {
1817 content.extend_from_slice(b"FINDME at the end\n");
1818 } else {
1819 content.extend_from_slice(format!("padding line {}\n", i).as_bytes());
1820 }
1821 }
1822 fs.write_file(&path, &content).unwrap();
1823
1824 let opts = make_search_opts(true);
1825 let mut cursor = FileSearchCursor::new();
1826 let mut all_matches = Vec::new();
1827
1828 while !cursor.done {
1829 let batch = fs.search_file(&path, "FINDME", &opts, &mut cursor).unwrap();
1830 all_matches.extend(batch);
1831 }
1832
1833 assert_eq!(all_matches.len(), 1);
1834 assert_eq!(all_matches[0].line, total_lines); assert_eq!(all_matches[0].context, "FINDME at the end");
1836 }
1837
1838 #[test]
1839 fn test_search_file_end_offset_bounds_search() {
1840 let fs = StdFileSystem;
1841 let temp_dir = tempfile::tempdir().unwrap();
1842 let path = temp_dir.path().join("bounded.txt");
1843
1844 fs.write_file(&path, b"AAA\nBBB\nCCC\nDDD\n").unwrap();
1846
1847 let opts = make_search_opts(true);
1849 let mut cursor = FileSearchCursor::for_range(0, 8, 1);
1850 let mut matches = Vec::new();
1851 while !cursor.done {
1852 matches.extend(fs.search_file(&path, "AAA", &opts, &mut cursor).unwrap());
1853 }
1854 assert_eq!(matches.len(), 1);
1855 assert_eq!(matches[0].context, "AAA");
1856 assert_eq!(matches[0].line, 1);
1857
1858 let mut cursor = FileSearchCursor::for_range(0, 8, 1);
1860 let ccc = fs.search_file(&path, "CCC", &opts, &mut cursor).unwrap();
1861 assert!(ccc.is_empty(), "CCC should not be found in first 8 bytes");
1862
1863 let mut cursor = FileSearchCursor::for_range(8, 16, 3);
1865 let mut matches = Vec::new();
1866 while !cursor.done {
1867 matches.extend(fs.search_file(&path, "CCC", &opts, &mut cursor).unwrap());
1868 }
1869 assert_eq!(matches.len(), 1);
1870 assert_eq!(matches[0].context, "CCC");
1871 assert_eq!(matches[0].line, 3);
1872 }
1873}