1use std::borrow::Cow;
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom};
4use std::ops::Range;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex, atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}};
7use std::time::SystemTime;
8
9use crate::prettify::{self, PrettifyMode};
10
11pub trait Source: Send + Sync {
12 fn len(&self) -> usize;
13 fn is_empty(&self) -> bool { self.len() == 0 }
14 fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]>;
15 fn is_complete(&self) -> bool;
16 fn pump(&self) {}
19 fn revision(&self) -> u64 { 0 }
25
26 fn prettify_mode(&self) -> Option<PrettifyMode> { None }
30
31 fn prettify_label(&self) -> Option<String> { None }
35
36 fn set_prettify_mode(&self, _mode: PrettifyMode) {}
39
40 fn toggle_prettify(&self) {}
44
45 fn redetect_prettify(&self) {}
48
49 fn take_rotated(&self) -> bool { false }
54
55 fn path(&self) -> Option<&Path> { None }
59}
60
61pub fn find_tail_offset(src: &dyn Source, n: usize) -> usize {
69 let total = src.len();
70 if n == 0 || total == 0 {
71 return total;
72 }
73 let mut end = total;
76 if end > 0 && src.bytes((end - 1)..end)[0] == b'\n' {
77 end -= 1;
78 }
79
80 let chunk_size: usize = 64 * 1024;
81 let mut count = 0usize;
82 let mut pos = end;
83 while pos > 0 {
84 let chunk_start = pos.saturating_sub(chunk_size);
85 let bytes = src.bytes(chunk_start..pos);
86 for i in (0..bytes.len()).rev() {
87 if bytes[i] == b'\n' {
88 count += 1;
89 if count == n {
90 return chunk_start + i + 1;
91 }
92 }
93 }
94 pos = chunk_start;
95 }
96 0
97}
98
99pub struct FileSource {
100 mmap: Option<memmap2::Mmap>,
101 fallback_buf: Option<Vec<u8>>,
102 initial_size: usize,
103 appended_len: AtomicUsize,
104 streaming: Mutex<StreamingState>,
105 path: PathBuf,
107 known: Mutex<(u64, u64)>,
112 rotated: AtomicBool,
116}
117
118struct StreamingState {
119 file: File,
120 appended: Vec<u8>,
121}
122
123impl std::fmt::Debug for FileSource {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_struct("FileSource").finish()
126 }
127}
128
129impl FileSource {
130 pub fn open(path: &Path) -> std::io::Result<Self> {
131 let file = File::open(path)?;
132 let metadata = file.metadata()?;
133 if !metadata.is_file() {
134 return Err(std::io::Error::new(
135 std::io::ErrorKind::InvalidInput,
136 "not a regular file",
137 ));
138 }
139 let initial_size = metadata.len() as usize;
140 let inode = inode_of(&metadata);
141 let (mmap, fallback_buf) = if initial_size == 0 {
142 (None, Some(Vec::new()))
143 } else {
144 match unsafe { memmap2::Mmap::map(&file) } {
146 Ok(m) => (Some(m), None),
147 Err(_) => {
148 let mut buf = Vec::new();
149 let mut f = File::open(path)?;
150 f.read_to_end(&mut buf)?;
151 (None, Some(buf))
152 }
153 }
154 };
155 let mut stream_file = File::open(path)?;
158 stream_file.seek(SeekFrom::Start(initial_size as u64))?;
159 Ok(Self {
160 mmap,
161 fallback_buf,
162 initial_size,
163 appended_len: AtomicUsize::new(0),
164 streaming: Mutex::new(StreamingState {
165 file: stream_file,
166 appended: Vec::new(),
167 }),
168 path: path.to_path_buf(),
169 known: Mutex::new((initial_size as u64, inode)),
170 rotated: AtomicBool::new(false),
171 })
172 }
173
174 pub fn path(&self) -> &Path {
177 &self.path
178 }
179
180 pub fn take_rotated(&self) -> bool {
183 self.rotated.swap(false, Ordering::AcqRel)
184 }
185
186 fn static_bytes(&self) -> &[u8] {
187 if let Some(m) = &self.mmap {
188 &m[..]
189 } else if let Some(b) = &self.fallback_buf {
190 &b[..]
191 } else {
192 &[]
193 }
194 }
195}
196
197impl Source for FileSource {
198 fn len(&self) -> usize {
199 self.initial_size + self.appended_len.load(Ordering::Acquire)
200 }
201
202 fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
203 let static_bytes = self.static_bytes();
204 if range.end <= self.initial_size {
205 return Cow::Borrowed(&static_bytes[range]);
206 }
207 let stream = self.streaming.lock().unwrap();
208 let total = self.initial_size + stream.appended.len();
209 let start = range.start.min(total);
210 let end = range.end.min(total);
211 if start >= self.initial_size {
212 let off = start - self.initial_size;
213 let off_end = end - self.initial_size;
214 Cow::Owned(stream.appended[off..off_end].to_vec())
215 } else {
216 let mut v = Vec::with_capacity(end - start);
217 v.extend_from_slice(&static_bytes[start..self.initial_size]);
218 v.extend_from_slice(&stream.appended[..end - self.initial_size]);
219 Cow::Owned(v)
220 }
221 }
222
223 fn is_complete(&self) -> bool { true }
224
225 fn take_rotated(&self) -> bool {
226 Self::take_rotated(self)
227 }
228
229 fn path(&self) -> Option<&Path> {
230 Some(&self.path)
231 }
232
233 fn pump(&self) {
234 if let Ok(meta) = std::fs::metadata(&self.path) {
239 let new_size = meta.len();
240 let new_inode = inode_of(&meta);
241 let mut known = self.known.lock().unwrap();
242 let known_total = self.initial_size as u64 + self.appended_len.load(Ordering::Acquire) as u64;
243 let truncated = new_size < known_total;
244 let rotated = known.1 != 0 && new_inode != 0 && new_inode != known.1;
245 if truncated || rotated {
246 self.rotated.store(true, Ordering::Release);
247 known.0 = new_size;
250 known.1 = new_inode;
251 return;
252 }
253 known.0 = new_size;
254 known.1 = new_inode;
255 }
256
257 let mut stream = self.streaming.lock().unwrap();
258 let mut tmp = [0u8; 8192];
259 loop {
260 match stream.file.read(&mut tmp) {
261 Ok(0) => break,
262 Ok(n) => stream.appended.extend_from_slice(&tmp[..n]),
263 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
264 Err(_) => break,
265 }
266 }
267 let new_len = stream.appended.len();
268 self.appended_len.store(new_len, Ordering::Release);
269 }
270}
271
272#[cfg(unix)]
273fn inode_of(meta: &std::fs::Metadata) -> u64 {
274 use std::os::unix::fs::MetadataExt;
275 meta.ino()
276}
277
278#[cfg(not(unix))]
279fn inode_of(_meta: &std::fs::Metadata) -> u64 {
280 0
281}
282
283pub struct MockSource {
285 buf: Arc<Mutex<Vec<u8>>>,
286 complete: Arc<AtomicBool>,
287}
288
289impl Default for MockSource {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295impl MockSource {
296 pub fn new() -> Self {
297 Self {
298 buf: Arc::new(Mutex::new(Vec::new())),
299 complete: Arc::new(AtomicBool::new(false)),
300 }
301 }
302
303 pub fn append(&self, more: &[u8]) {
304 self.buf.lock().unwrap().extend_from_slice(more);
305 }
306
307 pub fn finish(&self) {
308 self.complete.store(true, Ordering::SeqCst);
309 }
310}
311
312impl Source for MockSource {
313 fn len(&self) -> usize { self.buf.lock().unwrap().len() }
314 fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
315 Cow::Owned(self.buf.lock().unwrap()[range].to_vec())
316 }
317 fn is_complete(&self) -> bool { self.complete.load(Ordering::SeqCst) }
318}
319
320pub struct LiveFileSource {
327 path: PathBuf,
328 state: Mutex<LiveState>,
329 revision: AtomicU64,
330}
331
332struct LiveState {
333 bytes: Vec<u8>,
334 signature: FileSignature,
335}
336
337#[derive(Clone, Copy, PartialEq, Eq, Debug)]
338struct FileSignature {
339 mtime: Option<SystemTime>,
340 size: u64,
341 ino: u64,
342}
343
344impl FileSignature {
345 fn read(path: &Path) -> std::io::Result<Self> {
346 let md = std::fs::metadata(path)?;
347 Ok(Self {
348 mtime: md.modified().ok(),
349 size: md.len(),
350 #[cfg(unix)]
351 ino: {
352 use std::os::unix::fs::MetadataExt;
353 md.ino()
354 },
355 #[cfg(not(unix))]
356 ino: 0,
357 })
358 }
359}
360
361impl std::fmt::Debug for LiveFileSource {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 f.debug_struct("LiveFileSource").field("path", &self.path).finish()
364 }
365}
366
367impl LiveFileSource {
368 pub fn open(path: &Path) -> std::io::Result<Self> {
369 let md = std::fs::metadata(path)?;
370 if !md.is_file() {
371 return Err(std::io::Error::new(
372 std::io::ErrorKind::InvalidInput,
373 "not a regular file",
374 ));
375 }
376 let bytes = std::fs::read(path)?;
377 let signature = FileSignature::read(path)?;
381 Ok(Self {
382 path: path.to_path_buf(),
383 state: Mutex::new(LiveState { bytes, signature }),
384 revision: AtomicU64::new(0),
385 })
386 }
387}
388
389impl Source for LiveFileSource {
390 fn len(&self) -> usize { self.state.lock().unwrap().bytes.len() }
391
392 fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
393 let s = self.state.lock().unwrap();
394 let end = range.end.min(s.bytes.len());
395 let start = range.start.min(end);
396 Cow::Owned(s.bytes[start..end].to_vec())
397 }
398
399 fn is_complete(&self) -> bool { false }
402
403 fn pump(&self) {
404 let new_sig = match FileSignature::read(&self.path) {
407 Ok(sig) => sig,
408 Err(_) => return,
409 };
410 let mut s = self.state.lock().unwrap();
411 if new_sig == s.signature {
412 return;
413 }
414 let new_bytes = match std::fs::read(&self.path) {
415 Ok(b) => b,
416 Err(_) => return,
417 };
418 let post_sig = FileSignature::read(&self.path).unwrap_or(new_sig);
420 s.bytes = new_bytes;
421 s.signature = post_sig;
422 drop(s);
423 self.revision.fetch_add(1, Ordering::AcqRel);
424 }
425
426 fn revision(&self) -> u64 { self.revision.load(Ordering::Acquire) }
427}
428
429pub struct TransformingSource {
434 inner: Box<dyn Source>,
435 state: Mutex<TransformState>,
436 revision: AtomicU64,
437}
438
439struct TransformState {
440 mode: PrettifyMode,
441 last_active: PrettifyMode,
445 cached: Vec<u8>,
448 last_error: Option<String>,
451}
452
453impl std::fmt::Debug for TransformingSource {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("TransformingSource").finish()
456 }
457}
458
459impl TransformingSource {
460 pub fn wrap(inner: Box<dyn Source>, mode: PrettifyMode) -> Self {
464 let raw = inner.bytes(0..inner.len()).to_vec();
465 let (cached, last_error) = run_transform(mode, &raw);
466 let last_active = if mode.is_active() { mode } else { PrettifyMode::Off };
467 Self {
468 inner,
469 state: Mutex::new(TransformState { mode, last_active, cached, last_error }),
470 revision: AtomicU64::new(0),
471 }
472 }
473
474 pub fn mode(&self) -> PrettifyMode {
475 self.state.lock().unwrap().mode
476 }
477
478 pub fn last_error(&self) -> Option<String> {
481 self.state.lock().unwrap().last_error.clone()
482 }
483
484 fn apply_mode(&self, mode: PrettifyMode) {
488 let raw = self.inner.bytes(0..self.inner.len()).to_vec();
489 let (cached, last_error) = run_transform(mode, &raw);
490 let mut s = self.state.lock().unwrap();
491 s.mode = mode;
492 if mode.is_active() && last_error.is_none() {
493 s.last_active = mode;
494 }
495 s.cached = cached;
496 s.last_error = last_error;
497 drop(s);
498 self.revision.fetch_add(1, Ordering::AcqRel);
499 }
500}
501
502fn run_transform(mode: PrettifyMode, raw: &[u8]) -> (Vec<u8>, Option<String>) {
503 match prettify::prettify(mode, raw) {
504 Ok(out) => (out, None),
505 Err(e) => (raw.to_vec(), Some(e)),
506 }
507}
508
509impl Source for TransformingSource {
510 fn len(&self) -> usize { self.state.lock().unwrap().cached.len() }
511
512 fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
513 let s = self.state.lock().unwrap();
514 let end = range.end.min(s.cached.len());
515 let start = range.start.min(end);
516 Cow::Owned(s.cached[start..end].to_vec())
517 }
518
519 fn is_complete(&self) -> bool { self.inner.is_complete() }
520
521 fn pump(&self) { self.inner.pump(); }
522
523 fn revision(&self) -> u64 { self.revision.load(Ordering::Acquire) }
524
525 fn prettify_mode(&self) -> Option<PrettifyMode> {
526 Some(self.state.lock().unwrap().mode)
527 }
528
529 fn prettify_label(&self) -> Option<String> {
530 let s = self.state.lock().unwrap();
531 if s.last_error.is_some() {
532 let lbl = s.mode.label();
534 let lbl = if lbl.is_empty() { s.last_active.label() } else { lbl };
535 Some(format!("{lbl}:err"))
536 } else if s.mode.is_active() {
537 Some(s.mode.label().to_string())
538 } else {
539 None
540 }
541 }
542
543 fn set_prettify_mode(&self, mode: PrettifyMode) {
544 self.apply_mode(mode);
545 }
546
547 fn toggle_prettify(&self) {
548 let target = {
549 let s = self.state.lock().unwrap();
550 if s.mode.is_active() {
551 PrettifyMode::Off
552 } else if s.last_active.is_active() {
553 s.last_active
554 } else {
555 drop(s);
559 self.redetect_prettify();
560 return;
561 }
562 };
563 self.apply_mode(target);
564 }
565
566 fn redetect_prettify(&self) {
567 let raw = self.inner.bytes(0..self.inner.len()).to_vec();
568 let detected = crate::prettify::detect_from_bytes(&raw);
569 if let Some(mode) = detected {
570 self.apply_mode(mode);
571 }
572 }
574}
575
576pub struct StdinSource {
577 inner: StdinInner,
578}
579
580enum StdinInner {
581 Static(Vec<u8>),
582 Streaming {
583 buf: Arc<Mutex<Vec<u8>>>,
584 len_cache: Arc<AtomicUsize>,
585 complete: Arc<AtomicBool>,
586 },
587}
588
589impl StdinSource {
590 pub fn read_all() -> std::io::Result<Self> {
594 let mut bytes = Vec::new();
595 std::io::stdin().lock().read_to_end(&mut bytes)?;
596 Ok(Self { inner: StdinInner::Static(bytes) })
597 }
598
599 #[cfg(unix)]
604 pub fn spawn_streaming() -> std::io::Result<Self> {
605 use std::os::unix::io::FromRawFd;
606 let cloned_fd = unsafe { libc::dup(libc::STDIN_FILENO) };
607 if cloned_fd < 0 {
608 return Err(std::io::Error::last_os_error());
609 }
610 let mut file = unsafe { File::from_raw_fd(cloned_fd) };
612
613 let buf = Arc::new(Mutex::new(Vec::<u8>::new()));
614 let len_cache = Arc::new(AtomicUsize::new(0));
615 let complete = Arc::new(AtomicBool::new(false));
616 let buf_w = Arc::clone(&buf);
617 let len_w = Arc::clone(&len_cache);
618 let complete_w = Arc::clone(&complete);
619 std::thread::spawn(move || {
620 let mut tmp = [0u8; 8192];
621 loop {
622 match file.read(&mut tmp) {
623 Ok(0) => break,
624 Ok(n) => {
625 let mut b = buf_w.lock().unwrap();
626 b.extend_from_slice(&tmp[..n]);
627 len_w.store(b.len(), Ordering::Release);
628 }
629 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
630 Err(_) => break,
631 }
632 }
633 complete_w.store(true, Ordering::SeqCst);
634 });
635 Ok(Self { inner: StdinInner::Streaming { buf, len_cache, complete } })
636 }
637}
638
639impl Source for StdinSource {
640 fn len(&self) -> usize {
641 match &self.inner {
642 StdinInner::Static(v) => v.len(),
643 StdinInner::Streaming { len_cache, .. } => len_cache.load(Ordering::Acquire),
644 }
645 }
646 fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
647 match &self.inner {
648 StdinInner::Static(v) => Cow::Borrowed(&v[range]),
649 StdinInner::Streaming { buf, .. } => Cow::Owned(buf.lock().unwrap()[range].to_vec()),
650 }
651 }
652 fn is_complete(&self) -> bool {
653 match &self.inner {
654 StdinInner::Static(_) => true,
655 StdinInner::Streaming { complete, .. } => complete.load(Ordering::Acquire),
656 }
657 }
658}
659
660pub struct MemorySource {
663 bytes: Vec<u8>,
664}
665
666impl MemorySource {
667 pub fn new(bytes: Vec<u8>) -> Self {
668 Self { bytes }
669 }
670}
671
672impl Source for MemorySource {
673 fn len(&self) -> usize {
674 self.bytes.len()
675 }
676
677 fn bytes(&self, range: std::ops::Range<usize>) -> std::borrow::Cow<'_, [u8]> {
678 std::borrow::Cow::Borrowed(&self.bytes[range])
679 }
680
681 fn is_complete(&self) -> bool {
682 true
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use std::io::Write;
690
691 #[test]
692 fn file_source_reads_temp_file() {
693 let mut tmp = tempfile::NamedTempFile::new().unwrap();
694 tmp.write_all(b"hello world").unwrap();
695 let src = FileSource::open(tmp.path()).unwrap();
696 assert_eq!(src.len(), 11);
697 assert_eq!(&*src.bytes(0..5), b"hello");
698 assert_eq!(&*src.bytes(6..11), b"world");
699 assert!(src.is_complete());
700 }
701
702 #[test]
703 fn file_source_empty_file() {
704 let tmp = tempfile::NamedTempFile::new().unwrap();
705 let src = FileSource::open(tmp.path()).unwrap();
706 assert_eq!(src.len(), 0);
707 }
708
709 #[test]
710 fn file_source_directory_errors() {
711 let dir = tempfile::tempdir().unwrap();
712 let err = FileSource::open(dir.path()).unwrap_err();
713 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
714 }
715
716 #[test]
717 fn file_source_pump_picks_up_appended_bytes() {
718 let mut tmp = tempfile::NamedTempFile::new().unwrap();
719 tmp.write_all(b"first").unwrap();
720 tmp.flush().unwrap();
721 let src = FileSource::open(tmp.path()).unwrap();
722 assert_eq!(src.len(), 5);
723 tmp.write_all(b" second").unwrap();
725 tmp.flush().unwrap();
726 assert_eq!(src.len(), 5);
728 src.pump();
729 assert_eq!(src.len(), 12);
730 assert_eq!(&*src.bytes(0..5), b"first");
732 assert_eq!(&*src.bytes(5..12), b" second");
734 assert_eq!(&*src.bytes(3..10), b"st seco");
736 }
737
738 #[test]
739 fn find_tail_offset_zero_lines_returns_total() {
740 let m = MockSource::new();
741 m.append(b"a\nb\nc\n");
742 assert_eq!(find_tail_offset(&m, 0), 6);
743 }
744
745 #[test]
746 fn find_tail_offset_empty_source() {
747 let m = MockSource::new();
748 assert_eq!(find_tail_offset(&m, 5), 0);
749 }
750
751 #[test]
752 fn find_tail_offset_fewer_lines_than_n_returns_zero() {
753 let m = MockSource::new();
754 m.append(b"a\nb\nc\n"); assert_eq!(find_tail_offset(&m, 10), 0);
756 }
757
758 #[test]
759 fn find_tail_offset_last_one_with_trailing_newline() {
760 let m = MockSource::new();
761 m.append(b"alpha\nbeta\ngamma\n"); assert_eq!(find_tail_offset(&m, 1), 11);
764 }
765
766 #[test]
767 fn find_tail_offset_last_two_with_trailing_newline() {
768 let m = MockSource::new();
769 m.append(b"alpha\nbeta\ngamma\n");
770 assert_eq!(find_tail_offset(&m, 2), 6);
772 }
773
774 #[test]
775 fn find_tail_offset_last_one_no_trailing_newline() {
776 let m = MockSource::new();
777 m.append(b"alpha\nbeta\ngamma"); assert_eq!(find_tail_offset(&m, 1), 11);
780 }
781
782 #[test]
783 fn find_tail_offset_exactly_n_lines_returns_zero() {
784 let m = MockSource::new();
785 m.append(b"a\nb\nc\n"); assert_eq!(find_tail_offset(&m, 3), 0);
787 }
788
789 #[test]
790 fn live_source_reads_initial_content() {
791 let mut tmp = tempfile::NamedTempFile::new().unwrap();
792 tmp.write_all(b"alpha\nbeta\n").unwrap();
793 tmp.flush().unwrap();
794 let src = LiveFileSource::open(tmp.path()).unwrap();
795 assert_eq!(src.len(), 11);
796 assert_eq!(&*src.bytes(0..11), b"alpha\nbeta\n");
797 assert_eq!(src.revision(), 0);
798 assert!(!src.is_complete()); }
800
801 #[test]
802 fn live_source_pump_picks_up_rewritten_content() {
803 let tmp = tempfile::NamedTempFile::new().unwrap();
804 std::fs::write(tmp.path(), b"first\n").unwrap();
805 let src = LiveFileSource::open(tmp.path()).unwrap();
806 assert_eq!(src.len(), 6);
807 assert_eq!(src.revision(), 0);
808
809 std::thread::sleep(std::time::Duration::from_millis(20));
812 std::fs::write(tmp.path(), b"second longer line\n").unwrap();
813 src.pump();
814 assert_eq!(src.len(), 19);
815 assert_eq!(&*src.bytes(0..19), b"second longer line\n");
816 assert_eq!(src.revision(), 1);
817 }
818
819 #[test]
820 fn live_source_pump_no_change_does_not_bump_revision() {
821 let tmp = tempfile::NamedTempFile::new().unwrap();
822 std::fs::write(tmp.path(), b"stable\n").unwrap();
823 let src = LiveFileSource::open(tmp.path()).unwrap();
824 let r0 = src.revision();
825 src.pump();
826 src.pump();
827 src.pump();
828 assert_eq!(src.revision(), r0);
829 }
830
831 #[test]
832 fn live_source_handles_file_shrink() {
833 let tmp = tempfile::NamedTempFile::new().unwrap();
834 std::fs::write(tmp.path(), b"longer initial content\n").unwrap();
835 let src = LiveFileSource::open(tmp.path()).unwrap();
836 assert!(src.len() > 5);
837 std::thread::sleep(std::time::Duration::from_millis(20));
838 std::fs::write(tmp.path(), b"x\n").unwrap();
839 src.pump();
840 assert_eq!(src.len(), 2);
841 assert_eq!(&*src.bytes(0..2), b"x\n");
842 assert_eq!(src.revision(), 1);
843 }
844
845 #[test]
846 fn live_source_handles_atomic_rename() {
847 let dir = tempfile::tempdir().unwrap();
850 let target = dir.path().join("file.txt");
851 std::fs::write(&target, b"original\n").unwrap();
852 let src = LiveFileSource::open(&target).unwrap();
853 assert_eq!(&*src.bytes(0..9), b"original\n");
854
855 std::thread::sleep(std::time::Duration::from_millis(20));
856 let staging = dir.path().join("file.txt.tmp");
857 std::fs::write(&staging, b"renamed in\n").unwrap();
858 std::fs::rename(&staging, &target).unwrap();
859 src.pump();
860 assert_eq!(src.len(), 11);
861 assert_eq!(&*src.bytes(0..11), b"renamed in\n");
862 assert_eq!(src.revision(), 1);
863 }
864
865 #[test]
866 fn live_source_rebuild_flow_against_line_index() {
867 use crate::line_index::LineIndex;
868
869 let tmp = tempfile::NamedTempFile::new().unwrap();
870 std::fs::write(tmp.path(), b"a\nb\nc\n").unwrap();
871 let src = LiveFileSource::open(tmp.path()).unwrap();
872
873 let mut idx = LineIndex::new();
874 idx.notice_new_bytes(&src);
875 assert_eq!(idx.line_count(), 3);
876 let r0 = src.revision();
877
878 std::thread::sleep(std::time::Duration::from_millis(20));
880 std::fs::write(tmp.path(), b"one\ntwo\nthree\nfour\nfive\n").unwrap();
881 src.pump();
882 assert_ne!(src.revision(), r0, "revision must bump on rewrite");
883
884 idx = LineIndex::new();
886 idx.notice_new_bytes(&src);
887 assert_eq!(idx.line_count(), 5);
888 assert_eq!(&*src.bytes(idx.line_range(2, &src)), b"three");
889 }
890
891 #[test]
892 fn transforming_source_passes_through_when_off() {
893 let inner = MockSource::new();
894 inner.append(b"hello\nworld\n");
895 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Off);
896 assert_eq!(&*t.bytes(0..t.len()), b"hello\nworld\n");
897 assert!(t.last_error().is_none());
898 assert_eq!(t.revision(), 0);
899 }
900
901 #[test]
902 fn transforming_source_emits_pretty_bytes_when_on() {
903 let inner = MockSource::new();
904 inner.append(b"{\"a\":1,\"b\":2}");
905 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
906 let out = t.bytes(0..t.len()).to_vec();
907 let s = String::from_utf8(out).unwrap();
908 assert!(s.contains("\"a\": 1"));
909 assert!(s.contains("\"b\": 2"));
910 assert!(t.last_error().is_none());
911 }
912
913 #[test]
914 fn transforming_source_revision_bumps_on_mode_change() {
915 let inner = MockSource::new();
916 inner.append(b"{\"x\":1}");
917 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Off);
918 let r0 = t.revision();
919 t.set_prettify_mode(PrettifyMode::Json);
920 assert!(t.revision() > r0);
921 let r1 = t.revision();
922 t.set_prettify_mode(PrettifyMode::Off);
923 assert!(t.revision() > r1);
924 }
925
926 #[test]
927 fn transforming_source_falls_back_to_raw_on_parse_error() {
928 let inner = MockSource::new();
929 inner.append(b"not actually json");
930 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
931 assert_eq!(&*t.bytes(0..t.len()), b"not actually json");
933 let err = t.last_error().expect("expected parse error to be surfaced");
934 assert!(err.contains("json"), "expected json in error, got: {err}");
935 let label = t.prettify_label().expect("error → label should be set");
937 assert!(label.ends_with(":err"), "expected :err label, got: {label}");
938 }
939
940 #[test]
941 fn transforming_source_set_mode_recovers_from_error() {
942 let inner = MockSource::new();
943 inner.append(b"plain content");
944 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
945 assert!(t.last_error().is_some());
946 t.set_prettify_mode(PrettifyMode::Off);
947 assert!(t.last_error().is_none());
948 assert_eq!(&*t.bytes(0..t.len()), b"plain content");
949 }
950
951 #[test]
952 fn transforming_source_toggle_flips_between_active_and_off() {
953 let inner = MockSource::new();
954 inner.append(b"{\"x\":1}");
955 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
956 assert_eq!(t.mode(), PrettifyMode::Json);
957 t.toggle_prettify();
958 assert_eq!(t.mode(), PrettifyMode::Off);
959 t.toggle_prettify();
960 assert_eq!(t.mode(), PrettifyMode::Json);
962 }
963
964 #[test]
965 fn transforming_source_redetect_picks_up_format() {
966 let inner = MockSource::new();
967 inner.append(b"<?xml version=\"1.0\"?><root/>");
968 let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Off);
969 t.redetect_prettify();
971 assert_eq!(t.mode(), PrettifyMode::Xml);
972 }
973
974 #[test]
975 fn live_source_directory_errors() {
976 let dir = tempfile::tempdir().unwrap();
977 let err = LiveFileSource::open(dir.path()).unwrap_err();
978 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
979 }
980
981 #[test]
982 fn mock_source_grows_and_finishes() {
983 let m = MockSource::new();
984 assert_eq!(m.len(), 0);
985 assert!(!m.is_complete());
986 m.append(b"abc");
987 assert_eq!(m.len(), 3);
988 assert_eq!(&*m.bytes(0..3), b"abc");
989 m.append(b"def");
990 assert_eq!(&*m.bytes(0..6), b"abcdef");
991 m.finish();
992 assert!(m.is_complete());
993 }
994
995 #[test]
996 fn memory_source_len_and_bytes() {
997 let src = MemorySource::new(b"hello world".to_vec());
998 assert_eq!(src.len(), 11);
999 let slice = src.bytes(0..5);
1000 assert_eq!(&*slice, b"hello");
1001 assert!(src.is_complete());
1002 }
1003
1004 #[test]
1005 fn memory_source_empty() {
1006 let src = MemorySource::new(Vec::new());
1007 assert_eq!(src.len(), 0);
1008 assert!(src.is_empty());
1009 }
1010}