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 home_dir(&self) -> io::Result<PathBuf> {
637 dirs::home_dir()
638 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))
639 }
640
641 fn search_file(
658 &self,
659 path: &Path,
660 pattern: &str,
661 opts: &FileSearchOptions,
662 cursor: &mut FileSearchCursor,
663 ) -> io::Result<Vec<SearchMatch>>;
664
665 fn sudo_write(&self, path: &Path, data: &[u8], mode: u32, uid: u32, gid: u32)
676 -> io::Result<()>;
677}
678
679pub trait FileSystemExt: FileSystem {
704 fn read_file_async(
706 &self,
707 path: &Path,
708 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
709 async { self.read_file(path) }
710 }
711
712 fn read_range_async(
714 &self,
715 path: &Path,
716 offset: u64,
717 len: usize,
718 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
719 async move { self.read_range(path, offset, len) }
720 }
721
722 fn count_line_feeds_in_range_async(
724 &self,
725 path: &Path,
726 offset: u64,
727 len: usize,
728 ) -> impl std::future::Future<Output = io::Result<usize>> + Send {
729 async move { self.count_line_feeds_in_range(path, offset, len) }
730 }
731
732 fn write_file_async(
734 &self,
735 path: &Path,
736 data: &[u8],
737 ) -> impl std::future::Future<Output = io::Result<()>> + Send {
738 async { self.write_file(path, data) }
739 }
740
741 fn metadata_async(
743 &self,
744 path: &Path,
745 ) -> impl std::future::Future<Output = io::Result<FileMetadata>> + Send {
746 async { self.metadata(path) }
747 }
748
749 fn exists_async(&self, path: &Path) -> impl std::future::Future<Output = bool> + Send {
751 async { self.exists(path) }
752 }
753
754 fn is_dir_async(
756 &self,
757 path: &Path,
758 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
759 async { self.is_dir(path) }
760 }
761
762 fn is_file_async(
764 &self,
765 path: &Path,
766 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
767 async { self.is_file(path) }
768 }
769
770 fn read_dir_async(
772 &self,
773 path: &Path,
774 ) -> impl std::future::Future<Output = io::Result<Vec<DirEntry>>> + Send {
775 async { self.read_dir(path) }
776 }
777
778 fn canonicalize_async(
780 &self,
781 path: &Path,
782 ) -> impl std::future::Future<Output = io::Result<PathBuf>> + Send {
783 async { self.canonicalize(path) }
784 }
785}
786
787impl<T: FileSystem> FileSystemExt for T {}
789
790pub fn build_search_regex(
796 pattern: &str,
797 opts: &FileSearchOptions,
798) -> io::Result<regex::bytes::Regex> {
799 let re_pattern = if opts.fixed_string {
800 regex::escape(pattern)
801 } else {
802 pattern.to_string()
803 };
804 let re_pattern = if opts.whole_word {
805 format!(r"\b{}\b", re_pattern)
806 } else {
807 re_pattern
808 };
809 let re_pattern = if opts.case_sensitive {
810 re_pattern
811 } else {
812 format!("(?i){}", re_pattern)
813 };
814 regex::bytes::Regex::new(&re_pattern)
815 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))
816}
817
818pub fn default_search_file(
822 fs: &dyn FileSystem,
823 path: &Path,
824 pattern: &str,
825 opts: &FileSearchOptions,
826 cursor: &mut FileSearchCursor,
827) -> io::Result<Vec<SearchMatch>> {
828 if cursor.done {
829 return Ok(vec![]);
830 }
831
832 const CHUNK_SIZE: usize = 1_048_576; let overlap = pattern.len().max(256);
834
835 let file_len = fs.metadata(path)?.size as usize;
836 let effective_end = cursor.end_offset.unwrap_or(file_len).min(file_len);
837
838 if cursor.offset == 0 && cursor.end_offset.is_none() {
840 if file_len == 0 {
841 cursor.done = true;
842 return Ok(vec![]);
843 }
844 let header_len = file_len.min(8192);
845 let header = fs.read_range(path, 0, header_len)?;
846 if header.contains(&0) {
847 cursor.done = true;
848 return Ok(vec![]);
849 }
850 }
851
852 if cursor.offset >= effective_end {
853 cursor.done = true;
854 return Ok(vec![]);
855 }
856
857 let regex = build_search_regex(pattern, opts)?;
858
859 let read_start = cursor.offset.saturating_sub(overlap);
861 let read_end = (read_start + CHUNK_SIZE).min(effective_end);
862 let chunk = fs.read_range(path, read_start as u64, read_end - read_start)?;
863
864 let overlap_len = cursor.offset - read_start;
865
866 let newlines_in_overlap = chunk[..overlap_len].iter().filter(|&&b| b == b'\n').count();
868 let mut line_at = cursor.running_line.saturating_sub(newlines_in_overlap);
869 let mut counted_to = 0usize;
870 let mut matches = Vec::new();
871
872 for m in regex.find_iter(&chunk) {
873 if overlap_len > 0 && m.end() <= overlap_len {
875 continue;
876 }
877 if matches.len() >= opts.max_matches {
878 break;
879 }
880
881 line_at += chunk[counted_to..m.start()]
883 .iter()
884 .filter(|&&b| b == b'\n')
885 .count();
886 counted_to = m.start();
887
888 let line_start = chunk[..m.start()]
890 .iter()
891 .rposition(|&b| b == b'\n')
892 .map(|p| p + 1)
893 .unwrap_or(0);
894 let line_end = chunk[m.start()..]
895 .iter()
896 .position(|&b| b == b'\n')
897 .map(|p| m.start() + p)
898 .unwrap_or(chunk.len());
899
900 let column = m.start() - line_start + 1;
901 let context = String::from_utf8_lossy(&chunk[line_start..line_end]).into_owned();
902
903 matches.push(SearchMatch {
904 byte_offset: read_start + m.start(),
905 length: m.end() - m.start(),
906 line: line_at,
907 column,
908 context,
909 });
910 }
911
912 let new_data = &chunk[overlap_len..];
914 cursor.running_line += new_data.iter().filter(|&&b| b == b'\n').count();
915 cursor.offset = read_end;
916 if read_end >= effective_end {
917 cursor.done = true;
918 }
919
920 Ok(matches)
921}
922
923#[derive(Debug, Clone, Copy, Default)]
931pub struct StdFileSystem;
932
933impl StdFileSystem {
934 fn is_hidden(path: &Path) -> bool {
936 path.file_name()
937 .and_then(|n| n.to_str())
938 .is_some_and(|n| n.starts_with('.'))
939 }
940
941 #[cfg(unix)]
943 pub fn current_user_groups() -> (u32, Vec<u32>) {
944 let euid = unsafe { libc::geteuid() };
946 let egid = unsafe { libc::getegid() };
947 let mut groups = vec![egid];
948
949 let ngroups = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
951 if ngroups > 0 {
952 let mut sup_groups = vec![0 as libc::gid_t; ngroups as usize];
953 let n = unsafe { libc::getgroups(ngroups, sup_groups.as_mut_ptr()) };
954 if n > 0 {
955 sup_groups.truncate(n as usize);
956 for g in sup_groups {
957 if g != egid {
958 groups.push(g);
959 }
960 }
961 }
962 }
963
964 (euid, groups)
965 }
966
967 fn build_metadata(path: &Path, meta: &std::fs::Metadata) -> FileMetadata {
969 #[cfg(unix)]
970 {
971 use std::os::unix::fs::MetadataExt;
972 let file_uid = meta.uid();
973 let file_gid = meta.gid();
974 let permissions = FilePermissions::from_std(meta.permissions());
975 let (euid, user_groups) = Self::current_user_groups();
976 let is_readonly =
977 permissions.is_readonly_for_user(euid, file_uid, file_gid, &user_groups);
978 FileMetadata {
979 size: meta.len(),
980 modified: meta.modified().ok(),
981 permissions: Some(permissions),
982 is_hidden: Self::is_hidden(path),
983 is_readonly,
984 uid: Some(file_uid),
985 gid: Some(file_gid),
986 }
987 }
988 #[cfg(not(unix))]
989 {
990 FileMetadata {
991 size: meta.len(),
992 modified: meta.modified().ok(),
993 permissions: Some(FilePermissions::from_std(meta.permissions())),
994 is_hidden: Self::is_hidden(path),
995 is_readonly: meta.permissions().readonly(),
996 }
997 }
998 }
999}
1000
1001impl FileSystem for StdFileSystem {
1002 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
1004 let data = std::fs::read(path)?;
1005 crate::services::counters::global().inc_disk_bytes_read(data.len() as u64);
1006 Ok(data)
1007 }
1008
1009 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
1010 let mut file = std::fs::File::open(path)?;
1011 file.seek(io::SeekFrom::Start(offset))?;
1012 let mut buffer = vec![0u8; len];
1013 file.read_exact(&mut buffer)?;
1014 crate::services::counters::global().inc_disk_bytes_read(len as u64);
1015 Ok(buffer)
1016 }
1017
1018 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1019 let original_metadata = self.metadata_if_exists(path);
1020 let temp_path = self.temp_path_for(path);
1021 {
1022 let mut file = self.create_file(&temp_path)?;
1023 file.write_all(data)?;
1024 file.sync_all()?;
1025 }
1026 if let Some(ref meta) = original_metadata {
1027 if let Some(ref perms) = meta.permissions {
1028 #[allow(clippy::let_underscore_must_use)]
1030 let _ = self.set_permissions(&temp_path, perms);
1031 }
1032 }
1033 self.rename(&temp_path, path)?;
1034 Ok(())
1035 }
1036
1037 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
1038 let file = std::fs::File::create(path)?;
1039 Ok(Box::new(StdFileWriter(file)))
1040 }
1041
1042 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
1043 let file = std::fs::File::open(path)?;
1044 Ok(Box::new(StdFileReader(file)))
1045 }
1046
1047 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
1048 let file = std::fs::OpenOptions::new()
1049 .write(true)
1050 .truncate(true)
1051 .open(path)?;
1052 Ok(Box::new(StdFileWriter(file)))
1053 }
1054
1055 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
1056 let file = std::fs::OpenOptions::new()
1057 .create(true)
1058 .append(true)
1059 .open(path)?;
1060 Ok(Box::new(StdFileWriter(file)))
1061 }
1062
1063 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
1064 let file = std::fs::OpenOptions::new().write(true).open(path)?;
1065 file.set_len(len)
1066 }
1067
1068 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1070 std::fs::rename(from, to)
1071 }
1072
1073 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
1074 std::fs::copy(from, to)
1075 }
1076
1077 fn remove_file(&self, path: &Path) -> io::Result<()> {
1078 std::fs::remove_file(path)
1079 }
1080
1081 fn remove_dir(&self, path: &Path) -> io::Result<()> {
1082 std::fs::remove_dir(path)
1083 }
1084
1085 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
1087 let meta = std::fs::metadata(path)?;
1088 Ok(Self::build_metadata(path, &meta))
1089 }
1090
1091 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
1092 let meta = std::fs::symlink_metadata(path)?;
1093 Ok(Self::build_metadata(path, &meta))
1094 }
1095
1096 fn is_dir(&self, path: &Path) -> io::Result<bool> {
1097 Ok(std::fs::metadata(path)?.is_dir())
1098 }
1099
1100 fn is_file(&self, path: &Path) -> io::Result<bool> {
1101 Ok(std::fs::metadata(path)?.is_file())
1102 }
1103
1104 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
1105 std::fs::set_permissions(path, permissions.to_std())
1106 }
1107
1108 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
1110 let mut entries = Vec::new();
1111 for entry in std::fs::read_dir(path)? {
1112 let entry = entry?;
1113 let path = entry.path();
1114 let name = entry.file_name().to_string_lossy().into_owned();
1115 let file_type = entry.file_type()?;
1116
1117 let entry_type = if file_type.is_dir() {
1118 EntryType::Directory
1119 } else if file_type.is_symlink() {
1120 EntryType::Symlink
1121 } else {
1122 EntryType::File
1123 };
1124
1125 let mut dir_entry = DirEntry::new(path.clone(), name, entry_type);
1126
1127 if file_type.is_symlink() {
1129 dir_entry.symlink_target_is_dir = std::fs::metadata(&path)
1130 .map(|m| m.is_dir())
1131 .unwrap_or(false);
1132 }
1133
1134 entries.push(dir_entry);
1135 }
1136 Ok(entries)
1137 }
1138
1139 fn create_dir(&self, path: &Path) -> io::Result<()> {
1140 std::fs::create_dir(path)
1141 }
1142
1143 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
1144 std::fs::create_dir_all(path)
1145 }
1146
1147 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
1149 std::fs::canonicalize(path)
1150 }
1151
1152 fn current_uid(&self) -> u32 {
1154 #[cfg(all(unix, feature = "runtime"))]
1155 {
1156 unsafe { libc::getuid() }
1158 }
1159 #[cfg(not(all(unix, feature = "runtime")))]
1160 {
1161 0
1162 }
1163 }
1164
1165 fn sudo_write(
1166 &self,
1167 path: &Path,
1168 data: &[u8],
1169 mode: u32,
1170 uid: u32,
1171 gid: u32,
1172 ) -> io::Result<()> {
1173 use std::process::{Command, Stdio};
1174
1175 let mut child = Command::new("sudo")
1177 .args(["tee", &path.to_string_lossy()])
1178 .stdin(Stdio::piped())
1179 .stdout(Stdio::null())
1180 .stderr(Stdio::piped())
1181 .spawn()
1182 .map_err(|e| io::Error::other(format!("failed to spawn sudo: {}", e)))?;
1183
1184 if let Some(mut stdin) = child.stdin.take() {
1185 use std::io::Write;
1186 stdin.write_all(data)?;
1187 }
1188
1189 let output = child.wait_with_output()?;
1190 if !output.status.success() {
1191 let stderr = String::from_utf8_lossy(&output.stderr);
1192 return Err(io::Error::new(
1193 io::ErrorKind::PermissionDenied,
1194 format!("sudo tee failed: {}", stderr.trim()),
1195 ));
1196 }
1197
1198 let status = Command::new("sudo")
1200 .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()])
1201 .status()?;
1202 if !status.success() {
1203 return Err(io::Error::other("sudo chmod failed"));
1204 }
1205
1206 let status = Command::new("sudo")
1208 .args([
1209 "chown",
1210 &format!("{}:{}", uid, gid),
1211 &path.to_string_lossy(),
1212 ])
1213 .status()?;
1214 if !status.success() {
1215 return Err(io::Error::other("sudo chown failed"));
1216 }
1217
1218 Ok(())
1219 }
1220
1221 fn search_file(
1222 &self,
1223 path: &Path,
1224 pattern: &str,
1225 opts: &FileSearchOptions,
1226 cursor: &mut FileSearchCursor,
1227 ) -> io::Result<Vec<SearchMatch>> {
1228 default_search_file(self, path, pattern, opts, cursor)
1229 }
1230}
1231
1232#[derive(Debug, Clone, Copy, Default)]
1241pub struct NoopFileSystem;
1242
1243impl NoopFileSystem {
1244 fn unsupported<T>() -> io::Result<T> {
1245 Err(io::Error::new(
1246 io::ErrorKind::Unsupported,
1247 "Filesystem not available",
1248 ))
1249 }
1250}
1251
1252impl FileSystem for NoopFileSystem {
1253 fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
1254 Self::unsupported()
1255 }
1256
1257 fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> io::Result<Vec<u8>> {
1258 Self::unsupported()
1259 }
1260
1261 fn write_file(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1262 Self::unsupported()
1263 }
1264
1265 fn create_file(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
1266 Self::unsupported()
1267 }
1268
1269 fn open_file(&self, _path: &Path) -> io::Result<Box<dyn FileReader>> {
1270 Self::unsupported()
1271 }
1272
1273 fn open_file_for_write(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
1274 Self::unsupported()
1275 }
1276
1277 fn open_file_for_append(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
1278 Self::unsupported()
1279 }
1280
1281 fn set_file_length(&self, _path: &Path, _len: u64) -> io::Result<()> {
1282 Self::unsupported()
1283 }
1284
1285 fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1286 Self::unsupported()
1287 }
1288
1289 fn copy(&self, _from: &Path, _to: &Path) -> io::Result<u64> {
1290 Self::unsupported()
1291 }
1292
1293 fn remove_file(&self, _path: &Path) -> io::Result<()> {
1294 Self::unsupported()
1295 }
1296
1297 fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1298 Self::unsupported()
1299 }
1300
1301 fn metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
1302 Self::unsupported()
1303 }
1304
1305 fn symlink_metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
1306 Self::unsupported()
1307 }
1308
1309 fn is_dir(&self, _path: &Path) -> io::Result<bool> {
1310 Self::unsupported()
1311 }
1312
1313 fn is_file(&self, _path: &Path) -> io::Result<bool> {
1314 Self::unsupported()
1315 }
1316
1317 fn set_permissions(&self, _path: &Path, _permissions: &FilePermissions) -> io::Result<()> {
1318 Self::unsupported()
1319 }
1320
1321 fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1322 Self::unsupported()
1323 }
1324
1325 fn create_dir(&self, _path: &Path) -> io::Result<()> {
1326 Self::unsupported()
1327 }
1328
1329 fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1330 Self::unsupported()
1331 }
1332
1333 fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1334 Self::unsupported()
1335 }
1336
1337 fn current_uid(&self) -> u32 {
1338 0
1339 }
1340
1341 fn search_file(
1342 &self,
1343 _path: &Path,
1344 _pattern: &str,
1345 _opts: &FileSearchOptions,
1346 _cursor: &mut FileSearchCursor,
1347 ) -> io::Result<Vec<SearchMatch>> {
1348 Self::unsupported()
1349 }
1350
1351 fn sudo_write(
1352 &self,
1353 _path: &Path,
1354 _data: &[u8],
1355 _mode: u32,
1356 _uid: u32,
1357 _gid: u32,
1358 ) -> io::Result<()> {
1359 Self::unsupported()
1360 }
1361}
1362
1363#[cfg(test)]
1368mod tests {
1369 use super::*;
1370 use tempfile::NamedTempFile;
1371
1372 #[test]
1373 fn test_std_filesystem_read_write() {
1374 let fs = StdFileSystem;
1375 let mut temp = NamedTempFile::new().unwrap();
1376 let path = temp.path().to_path_buf();
1377
1378 std::io::Write::write_all(&mut temp, b"Hello, World!").unwrap();
1379 std::io::Write::flush(&mut temp).unwrap();
1380
1381 let content = fs.read_file(&path).unwrap();
1382 assert_eq!(content, b"Hello, World!");
1383
1384 let range = fs.read_range(&path, 7, 5).unwrap();
1385 assert_eq!(range, b"World");
1386
1387 let meta = fs.metadata(&path).unwrap();
1388 assert_eq!(meta.size, 13);
1389 }
1390
1391 #[test]
1392 fn test_noop_filesystem() {
1393 let fs = NoopFileSystem;
1394 let path = Path::new("/some/path");
1395
1396 assert!(fs.read_file(path).is_err());
1397 assert!(fs.read_range(path, 0, 10).is_err());
1398 assert!(fs.write_file(path, b"data").is_err());
1399 assert!(fs.metadata(path).is_err());
1400 assert!(fs.read_dir(path).is_err());
1401 }
1402
1403 #[test]
1404 fn test_create_and_write_file() {
1405 let fs = StdFileSystem;
1406 let temp_dir = tempfile::tempdir().unwrap();
1407 let path = temp_dir.path().join("test.txt");
1408
1409 {
1410 let mut writer = fs.create_file(&path).unwrap();
1411 writer.write_all(b"test content").unwrap();
1412 writer.sync_all().unwrap();
1413 }
1414
1415 let content = fs.read_file(&path).unwrap();
1416 assert_eq!(content, b"test content");
1417 }
1418
1419 #[test]
1420 fn test_read_dir() {
1421 let fs = StdFileSystem;
1422 let temp_dir = tempfile::tempdir().unwrap();
1423
1424 fs.create_dir(&temp_dir.path().join("subdir")).unwrap();
1426 fs.write_file(&temp_dir.path().join("file1.txt"), b"content1")
1427 .unwrap();
1428 fs.write_file(&temp_dir.path().join("file2.txt"), b"content2")
1429 .unwrap();
1430
1431 let entries = fs.read_dir(temp_dir.path()).unwrap();
1432 assert_eq!(entries.len(), 3);
1433
1434 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1435 assert!(names.contains(&"subdir"));
1436 assert!(names.contains(&"file1.txt"));
1437 assert!(names.contains(&"file2.txt"));
1438 }
1439
1440 #[test]
1441 fn test_dir_entry_types() {
1442 let file = DirEntry::new(PathBuf::from("/file"), "file".to_string(), EntryType::File);
1443 assert!(file.is_file());
1444 assert!(!file.is_dir());
1445
1446 let dir = DirEntry::new(
1447 PathBuf::from("/dir"),
1448 "dir".to_string(),
1449 EntryType::Directory,
1450 );
1451 assert!(dir.is_dir());
1452 assert!(!dir.is_file());
1453
1454 let link_to_dir = DirEntry::new_symlink(PathBuf::from("/link"), "link".to_string(), true);
1455 assert!(link_to_dir.is_symlink());
1456 assert!(link_to_dir.is_dir());
1457 }
1458
1459 #[test]
1460 fn test_metadata_builder() {
1461 let meta = FileMetadata::default()
1462 .with_hidden(true)
1463 .with_readonly(true);
1464 assert!(meta.is_hidden);
1465 assert!(meta.is_readonly);
1466 }
1467
1468 #[test]
1469 fn test_atomic_write() {
1470 let fs = StdFileSystem;
1471 let temp_dir = tempfile::tempdir().unwrap();
1472 let path = temp_dir.path().join("atomic_test.txt");
1473
1474 fs.write_file(&path, b"initial").unwrap();
1475 assert_eq!(fs.read_file(&path).unwrap(), b"initial");
1476
1477 fs.write_file(&path, b"updated").unwrap();
1478 assert_eq!(fs.read_file(&path).unwrap(), b"updated");
1479 }
1480
1481 #[test]
1482 fn test_write_patched_default_impl() {
1483 let fs = StdFileSystem;
1485 let temp_dir = tempfile::tempdir().unwrap();
1486 let src_path = temp_dir.path().join("source.txt");
1487 let dst_path = temp_dir.path().join("dest.txt");
1488
1489 fs.write_file(&src_path, b"AAABBBCCC").unwrap();
1491
1492 let ops = vec![
1494 WriteOp::Copy { offset: 0, len: 3 }, WriteOp::Insert { data: b"XXX" }, WriteOp::Copy { offset: 6, len: 3 }, ];
1498
1499 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1500
1501 let result = fs.read_file(&dst_path).unwrap();
1502 assert_eq!(result, b"AAAXXXCCC");
1503 }
1504
1505 #[test]
1506 fn test_write_patched_same_file() {
1507 let fs = StdFileSystem;
1509 let temp_dir = tempfile::tempdir().unwrap();
1510 let path = temp_dir.path().join("file.txt");
1511
1512 fs.write_file(&path, b"Hello World").unwrap();
1514
1515 let ops = vec![
1517 WriteOp::Copy { offset: 0, len: 6 }, WriteOp::Insert { data: b"Rust" }, ];
1520
1521 fs.write_patched(&path, &path, &ops).unwrap();
1522
1523 let result = fs.read_file(&path).unwrap();
1524 assert_eq!(result, b"Hello Rust");
1525 }
1526
1527 #[test]
1528 fn test_write_patched_insert_only() {
1529 let fs = StdFileSystem;
1531 let temp_dir = tempfile::tempdir().unwrap();
1532 let src_path = temp_dir.path().join("empty.txt");
1533 let dst_path = temp_dir.path().join("new.txt");
1534
1535 fs.write_file(&src_path, b"").unwrap();
1537
1538 let ops = vec![WriteOp::Insert {
1539 data: b"All new content",
1540 }];
1541
1542 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1543
1544 let result = fs.read_file(&dst_path).unwrap();
1545 assert_eq!(result, b"All new content");
1546 }
1547
1548 fn make_search_opts(pattern_is_fixed: bool) -> FileSearchOptions {
1553 FileSearchOptions {
1554 fixed_string: pattern_is_fixed,
1555 case_sensitive: true,
1556 whole_word: false,
1557 max_matches: 100,
1558 }
1559 }
1560
1561 #[test]
1562 fn test_search_file_basic() {
1563 let fs = StdFileSystem;
1564 let temp_dir = tempfile::tempdir().unwrap();
1565 let path = temp_dir.path().join("test.txt");
1566 fs.write_file(&path, b"hello world\nfoo bar\nhello again\n")
1567 .unwrap();
1568
1569 let opts = make_search_opts(true);
1570 let mut cursor = FileSearchCursor::new();
1571 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1572
1573 assert!(cursor.done);
1574 assert_eq!(matches.len(), 2);
1575
1576 assert_eq!(matches[0].line, 1);
1577 assert_eq!(matches[0].column, 1);
1578 assert_eq!(matches[0].context, "hello world");
1579
1580 assert_eq!(matches[1].line, 3);
1581 assert_eq!(matches[1].column, 1);
1582 assert_eq!(matches[1].context, "hello again");
1583 }
1584
1585 #[test]
1586 fn test_search_file_no_matches() {
1587 let fs = StdFileSystem;
1588 let temp_dir = tempfile::tempdir().unwrap();
1589 let path = temp_dir.path().join("test.txt");
1590 fs.write_file(&path, b"hello world\n").unwrap();
1591
1592 let opts = make_search_opts(true);
1593 let mut cursor = FileSearchCursor::new();
1594 let matches = fs
1595 .search_file(&path, "NOTFOUND", &opts, &mut cursor)
1596 .unwrap();
1597
1598 assert!(cursor.done);
1599 assert!(matches.is_empty());
1600 }
1601
1602 #[test]
1603 fn test_search_file_case_insensitive() {
1604 let fs = StdFileSystem;
1605 let temp_dir = tempfile::tempdir().unwrap();
1606 let path = temp_dir.path().join("test.txt");
1607 fs.write_file(&path, b"Hello HELLO hello\n").unwrap();
1608
1609 let opts = FileSearchOptions {
1610 fixed_string: true,
1611 case_sensitive: false,
1612 whole_word: false,
1613 max_matches: 100,
1614 };
1615 let mut cursor = FileSearchCursor::new();
1616 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1617
1618 assert_eq!(matches.len(), 3);
1619 }
1620
1621 #[test]
1622 fn test_search_file_whole_word() {
1623 let fs = StdFileSystem;
1624 let temp_dir = tempfile::tempdir().unwrap();
1625 let path = temp_dir.path().join("test.txt");
1626 fs.write_file(&path, b"cat concatenate catalog\n").unwrap();
1627
1628 let opts = FileSearchOptions {
1629 fixed_string: true,
1630 case_sensitive: true,
1631 whole_word: true,
1632 max_matches: 100,
1633 };
1634 let mut cursor = FileSearchCursor::new();
1635 let matches = fs.search_file(&path, "cat", &opts, &mut cursor).unwrap();
1636
1637 assert_eq!(matches.len(), 1);
1638 assert_eq!(matches[0].column, 1);
1639 }
1640
1641 #[test]
1642 fn test_search_file_regex() {
1643 let fs = StdFileSystem;
1644 let temp_dir = tempfile::tempdir().unwrap();
1645 let path = temp_dir.path().join("test.txt");
1646 fs.write_file(&path, b"foo123 bar456 baz\n").unwrap();
1647
1648 let opts = FileSearchOptions {
1649 fixed_string: false,
1650 case_sensitive: true,
1651 whole_word: false,
1652 max_matches: 100,
1653 };
1654 let mut cursor = FileSearchCursor::new();
1655 let matches = fs
1656 .search_file(&path, r"[a-z]+\d+", &opts, &mut cursor)
1657 .unwrap();
1658
1659 assert_eq!(matches.len(), 2);
1660 assert_eq!(matches[0].context, "foo123 bar456 baz");
1661 }
1662
1663 #[test]
1664 fn test_search_file_binary_skipped() {
1665 let fs = StdFileSystem;
1666 let temp_dir = tempfile::tempdir().unwrap();
1667 let path = temp_dir.path().join("binary.dat");
1668 let mut data = b"hello world\n".to_vec();
1669 data.push(0); data.extend_from_slice(b"hello again\n");
1671 fs.write_file(&path, &data).unwrap();
1672
1673 let opts = make_search_opts(true);
1674 let mut cursor = FileSearchCursor::new();
1675 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1676
1677 assert!(cursor.done);
1678 assert!(matches.is_empty());
1679 }
1680
1681 #[test]
1682 fn test_search_file_empty_file() {
1683 let fs = StdFileSystem;
1684 let temp_dir = tempfile::tempdir().unwrap();
1685 let path = temp_dir.path().join("empty.txt");
1686 fs.write_file(&path, b"").unwrap();
1687
1688 let opts = make_search_opts(true);
1689 let mut cursor = FileSearchCursor::new();
1690 let matches = fs.search_file(&path, "hello", &opts, &mut cursor).unwrap();
1691
1692 assert!(cursor.done);
1693 assert!(matches.is_empty());
1694 }
1695
1696 #[test]
1697 fn test_search_file_max_matches() {
1698 let fs = StdFileSystem;
1699 let temp_dir = tempfile::tempdir().unwrap();
1700 let path = temp_dir.path().join("test.txt");
1701 fs.write_file(&path, b"aa bb aa cc aa dd aa\n").unwrap();
1702
1703 let opts = FileSearchOptions {
1704 fixed_string: true,
1705 case_sensitive: true,
1706 whole_word: false,
1707 max_matches: 2,
1708 };
1709 let mut cursor = FileSearchCursor::new();
1710 let matches = fs.search_file(&path, "aa", &opts, &mut cursor).unwrap();
1711
1712 assert_eq!(matches.len(), 2);
1713 }
1714
1715 #[test]
1716 fn test_search_file_cursor_multi_chunk() {
1717 let fs = StdFileSystem;
1718 let temp_dir = tempfile::tempdir().unwrap();
1719 let path = temp_dir.path().join("large.txt");
1720
1721 let mut content = Vec::new();
1723 for i in 0..100_000 {
1724 content.extend_from_slice(format!("line {} content here\n", i).as_bytes());
1725 }
1726 fs.write_file(&path, &content).unwrap();
1727
1728 let opts = FileSearchOptions {
1729 fixed_string: true,
1730 case_sensitive: true,
1731 whole_word: false,
1732 max_matches: 1000,
1733 };
1734 let mut cursor = FileSearchCursor::new();
1735 let mut all_matches = Vec::new();
1736
1737 while !cursor.done {
1738 let batch = fs
1739 .search_file(&path, "line 5000", &opts, &mut cursor)
1740 .unwrap();
1741 all_matches.extend(batch);
1742 }
1743
1744 assert_eq!(all_matches.len(), 11);
1747
1748 let first = &all_matches[0];
1750 assert_eq!(first.line, 5001); assert_eq!(first.column, 1);
1752 assert!(first.context.starts_with("line 5000"));
1753 }
1754
1755 #[test]
1756 fn test_search_file_cursor_no_duplicates() {
1757 let fs = StdFileSystem;
1758 let temp_dir = tempfile::tempdir().unwrap();
1759 let path = temp_dir.path().join("large.txt");
1760
1761 let mut content = Vec::new();
1763 for i in 0..100_000 {
1764 content.extend_from_slice(format!("MARKER_{:06}\n", i).as_bytes());
1765 }
1766 fs.write_file(&path, &content).unwrap();
1767
1768 let opts = FileSearchOptions {
1769 fixed_string: true,
1770 case_sensitive: true,
1771 whole_word: false,
1772 max_matches: 200_000,
1773 };
1774 let mut cursor = FileSearchCursor::new();
1775 let mut all_matches = Vec::new();
1776 let mut batches = 0;
1777
1778 while !cursor.done {
1779 let batch = fs
1780 .search_file(&path, "MARKER_", &opts, &mut cursor)
1781 .unwrap();
1782 all_matches.extend(batch);
1783 batches += 1;
1784 }
1785
1786 assert!(batches > 1, "Expected multiple batches, got {}", batches);
1788 assert_eq!(all_matches.len(), 100_000);
1790 let mut offsets: Vec<usize> = all_matches.iter().map(|m| m.byte_offset).collect();
1792 offsets.sort();
1793 offsets.dedup();
1794 assert_eq!(offsets.len(), 100_000);
1795 }
1796
1797 #[test]
1798 fn test_search_file_line_numbers_across_chunks() {
1799 let fs = StdFileSystem;
1800 let temp_dir = tempfile::tempdir().unwrap();
1801 let path = temp_dir.path().join("large.txt");
1802
1803 let mut content = Vec::new();
1805 let total_lines = 100_000;
1806 for i in 0..total_lines {
1807 if i == 99_999 {
1808 content.extend_from_slice(b"FINDME at the end\n");
1809 } else {
1810 content.extend_from_slice(format!("padding line {}\n", i).as_bytes());
1811 }
1812 }
1813 fs.write_file(&path, &content).unwrap();
1814
1815 let opts = make_search_opts(true);
1816 let mut cursor = FileSearchCursor::new();
1817 let mut all_matches = Vec::new();
1818
1819 while !cursor.done {
1820 let batch = fs.search_file(&path, "FINDME", &opts, &mut cursor).unwrap();
1821 all_matches.extend(batch);
1822 }
1823
1824 assert_eq!(all_matches.len(), 1);
1825 assert_eq!(all_matches[0].line, total_lines); assert_eq!(all_matches[0].context, "FINDME at the end");
1827 }
1828
1829 #[test]
1830 fn test_search_file_end_offset_bounds_search() {
1831 let fs = StdFileSystem;
1832 let temp_dir = tempfile::tempdir().unwrap();
1833 let path = temp_dir.path().join("bounded.txt");
1834
1835 fs.write_file(&path, b"AAA\nBBB\nCCC\nDDD\n").unwrap();
1837
1838 let opts = make_search_opts(true);
1840 let mut cursor = FileSearchCursor::for_range(0, 8, 1);
1841 let mut matches = Vec::new();
1842 while !cursor.done {
1843 matches.extend(fs.search_file(&path, "AAA", &opts, &mut cursor).unwrap());
1844 }
1845 assert_eq!(matches.len(), 1);
1846 assert_eq!(matches[0].context, "AAA");
1847 assert_eq!(matches[0].line, 1);
1848
1849 let mut cursor = FileSearchCursor::for_range(0, 8, 1);
1851 let ccc = fs.search_file(&path, "CCC", &opts, &mut cursor).unwrap();
1852 assert!(ccc.is_empty(), "CCC should not be found in first 8 bytes");
1853
1854 let mut cursor = FileSearchCursor::for_range(8, 16, 3);
1856 let mut matches = Vec::new();
1857 while !cursor.done {
1858 matches.extend(fs.search_file(&path, "CCC", &opts, &mut cursor).unwrap());
1859 }
1860 assert_eq!(matches.len(), 1);
1861 assert_eq!(matches[0].context, "CCC");
1862 assert_eq!(matches[0].line, 3);
1863 }
1864}