1use std::path::PathBuf;
6
7#[derive(Debug, Clone, Default)]
9pub struct ConfigOpts {
10 pub tailing: bool,
11 pub sticky: bool,
12 pub offset: i64,
13 pub offset_unit: OffsetUnit,
14 pub show_time: bool,
15 pub batch_window_ms: u64,
16 pub mode: InputMode,
17 pub force_chunked: bool,
18 pub disable_chunked: bool,
19 pub conservative: bool,
20 pub no_file_names: bool,
21 pub all_file_names: bool,
22 pub adaptive: bool,
23 pub strategy: Option<Strategy>,
24 pub max_memory: Option<usize>,
25 #[cfg(debug_assertions)]
26 pub profile_json: bool,
27}
28
29#[derive(Debug, Clone, Default, Copy)]
30pub enum OffsetUnit {
31 #[default]
32 Lines,
33 Blocks,
34 Bytes,
35}
36
37#[derive(Debug, Clone, Default)]
39pub enum InputMode {
40 #[default]
42 Stdin,
43 SingleFile { path: PathBuf },
45 MultiFile { paths: Vec<PathBuf> },
47}
48
49#[cfg(not(test))]
51mod runtime {
52 use std::sync::OnceLock;
53
54 use super::ConfigOpts;
55
56 pub static CONFIG: OnceLock<ConfigOpts> = OnceLock::new();
58
59 pub fn config() -> &'static ConfigOpts {
61 CONFIG
62 .get()
63 .expect("programmer error: tried to access configuration before it was set")
64 }
65
66 pub fn set(input: ConfigOpts) -> Result<(), Box<ConfigOpts>> {
68 match CONFIG.set(input) {
69 Ok(_) => Ok(()),
70 Err(e) => Err(Box::new(e)),
71 }
72 }
73}
74
75#[cfg(test)]
77mod runtime {
78 use std::cell::RefCell;
79
80 use super::ConfigOpts;
81
82 thread_local! {
84 pub static TEST_CONFIG: RefCell<Option<ConfigOpts>> = const { RefCell::new(None) };
85 }
86
87 pub fn config() -> ConfigOpts {
89 TEST_CONFIG.with(|cfg| cfg.borrow().as_ref().cloned().unwrap_or_else(ConfigOpts::default))
90 }
91
92 pub fn set(input: ConfigOpts) -> Result<(), Box<ConfigOpts>> {
94 TEST_CONFIG.with(|cfg| {
95 *cfg.borrow_mut() = Some(input);
96 });
97 Ok(())
98 }
99
100 pub fn update<F>(f: F)
102 where
103 F: FnOnce(&mut ConfigOpts),
104 {
105 TEST_CONFIG.with(|cfg| {
106 let mut borrowed = cfg.borrow_mut();
107 if borrowed.is_none() {
108 *borrowed = Some(ConfigOpts::default());
109 }
110 if let Some(ref mut config) = borrowed.as_mut() {
111 f(config);
112 }
113 });
114 }
115
116 pub fn with_config<F, R>(new_config: ConfigOpts, f: F) -> R
118 where
119 F: FnOnce() -> R,
120 {
121 let old_config = TEST_CONFIG.with(|cfg| cfg.borrow().clone());
123
124 TEST_CONFIG.with(|cfg| {
126 *cfg.borrow_mut() = Some(new_config);
127 });
128
129 let result = f();
131
132 TEST_CONFIG.with(|cfg| {
134 *cfg.borrow_mut() = old_config;
135 });
136
137 result
138 }
139}
140
141use miette::Result;
143pub use runtime::{config, set};
144#[cfg(test)]
145pub use runtime::{update, with_config};
146
147use crate::defaults::{SystemDefaults, get_system_config};
148use crate::errors::TaleError;
149use crate::readers::{AdaptiveStrategy, ConservativeStrategy, StaticStrategy, Strategy};
150
151pub fn tailing() -> bool {
153 #[cfg(not(test))]
154 return config().tailing;
155 #[cfg(test)]
156 return config().tailing;
157}
158
159pub fn sticky() -> bool {
160 #[cfg(not(test))]
161 return config().sticky;
162 #[cfg(test)]
163 return config().sticky;
164}
165
166pub fn offset() -> i64 {
167 #[cfg(not(test))]
168 return config().offset;
169 #[cfg(test)]
170 return config().offset;
171}
172
173pub fn offset_unit() -> OffsetUnit {
174 #[cfg(not(test))]
175 return config().offset_unit;
176 #[cfg(test)]
177 return config().offset_unit;
178}
179
180pub fn show_time() -> bool {
181 #[cfg(not(test))]
182 return config().show_time;
183 #[cfg(test)]
184 return config().show_time;
185}
186
187pub fn batch_window_ms() -> u64 {
188 #[cfg(not(test))]
189 return config().batch_window_ms;
190 #[cfg(test)]
191 return config().batch_window_ms;
192}
193
194pub fn force_chunked() -> bool {
195 #[cfg(not(test))]
196 return config().force_chunked;
197 #[cfg(test)]
198 return config().force_chunked;
199}
200
201pub fn disable_chunked() -> bool {
202 #[cfg(not(test))]
203 return config().disable_chunked;
204 #[cfg(test)]
205 return config().disable_chunked;
206}
207
208pub fn conservative() -> bool {
209 #[cfg(not(test))]
210 return config().conservative;
211 #[cfg(test)]
212 return config().conservative;
213}
214
215pub fn mode() -> InputMode {
216 #[cfg(not(test))]
217 return config().mode.clone();
218 #[cfg(test)]
219 return config().mode;
220}
221
222fn unescape_glob_pattern(pattern: &str) -> String {
224 let mut result = String::new();
225 let mut chars = pattern.chars().peekable();
226
227 while let Some(ch) = chars.next() {
228 if ch == '\\' {
229 if let Some(&next_ch) = chars.peek() {
231 if matches!(next_ch, '*' | '?' | '[' | ']' | '{' | '}') {
232 chars.next();
234 result.push(next_ch);
235 } else {
236 result.push(ch);
238 }
239 } else {
240 result.push(ch);
242 }
243 } else {
244 result.push(ch);
245 }
246 }
247
248 result
249}
250
251fn is_glob(maybe: &str) -> bool {
253 if maybe.contains('?') || maybe.contains('*') || maybe.contains('[') || maybe.contains('{') {
255 return true;
256 }
257
258 maybe.contains("\\*") || maybe.contains("\\?") || maybe.contains("\\[") || maybe.contains("\\{")
260}
261
262fn expand_globs(args: &[String]) -> Result<Vec<PathBuf>, TaleError> {
266 let mut all_paths = Vec::new();
267
268 for candidate in args {
269 if is_glob(candidate.as_str()) {
270 let unescaped_pattern = unescape_glob_pattern(candidate);
272 let pattern = glob::glob(&unescaped_pattern)?;
273 for fpath in pattern.flatten() {
274 if fpath.is_file() {
275 all_paths.push(fpath);
276 }
277 }
278 } else {
279 let fpath = PathBuf::from(candidate);
280 if fpath.exists() && fpath.is_file() {
281 all_paths.push(fpath);
282 }
283 }
284 }
285 all_paths.sort();
286 Ok(all_paths)
287}
288
289fn handle_possible_paths(args: &[String]) -> Result<Vec<PathBuf>, TaleError> {
290 match expand_globs(args) {
291 Ok(paths) => {
292 if paths.is_empty() {
293 let patterns: Vec<String> = args.iter().map(|s| format!("'{}'", s)).collect();
295 Err(TaleError::from(Box::new(crate::errors::FileError::NotFound {
296 path: PathBuf::from(patterns.join(", ")),
297 similar_files: vec![
298 "Check if the glob pattern is correct".to_string(),
299 "Verify the files exist in the specified directory".to_string(),
300 "Try using an absolute path".to_string(),
301 ],
302 })))
303 } else {
304 Ok(paths)
305 }
306 }
307 Err(e) => {
308 Err(e)
310 }
311 }
312}
313
314impl ConfigOpts {
315 pub fn new(args: &crate::Args) -> Result<Self> {
316 let system_config = get_system_config();
318 let (mode, maybe_offset) = match args.args.len() {
319 0 => (InputMode::Stdin, None),
320 1 => {
321 let only = &args.args[0];
322 if (only.starts_with('-') || only.starts_with('+'))
323 && only.len() > 1
324 && let Ok(offset) = only.parse::<i64>()
325 {
326 (InputMode::Stdin, Some(offset))
328 } else {
329 if is_glob(only) {
331 let paths = handle_possible_paths(vec![only.clone()].as_slice())?;
333 (InputMode::MultiFile { paths }, None)
334 } else {
335 (
337 InputMode::SingleFile {
338 path: PathBuf::from(only),
339 },
340 None,
341 )
342 }
343 }
344 }
345 2 => {
346 let (first, second) = (&args.args[0], &args.args[1]);
347
348 if let Ok(offset) = first.parse::<i64>() {
350 (
352 InputMode::SingleFile {
353 path: PathBuf::from(second),
354 },
355 Some(offset),
356 )
357 } else {
358 let paths = handle_possible_paths(args.args.as_slice())?;
360 (InputMode::MultiFile { paths }, None)
361 }
362 }
363 _ => {
364 let paths = handle_possible_paths(args.args.as_slice())?;
367 (InputMode::MultiFile { paths }, None)
368 }
369 };
370
371 let (offset, offset_unit) = if let Some(blocks) = args.blocks {
372 (blocks, OffsetUnit::Blocks)
373 } else if let Some(bytes) = args.bytes {
374 (bytes, OffsetUnit::Bytes)
375 } else if let Some(lines) = args.offset {
376 (lines, OffsetUnit::Lines)
377 } else if let Some(offset) = maybe_offset {
378 (offset, OffsetUnit::Lines)
379 } else {
380 (0, OffsetUnit::Lines)
381 };
382
383 let max_memory = args.max_memory.unwrap_or_else(|| {
385 let system_percentage = system_config.memory_percentage;
387 if let Some(memory_stats) = memory_stats::memory_stats() {
388 let system_memory = memory_stats.physical_mem;
389 let calculated = (system_memory as f64 * system_percentage / 100.0) as usize;
390 calculated.clamp(SystemDefaults::MIN_MEMORY_BUDGET, SystemDefaults::MAX_MEMORY_BUDGET)
391 } else {
392 system_config.max_memory_mb * 1024 * 1024
394 }
395 });
396
397 #[cfg(debug_assertions)]
398 let stratarg = args.chunk_strategy.clone();
399 #[cfg(not(debug_assertions))]
400 let stratarg = None;
401
402 let strategy = stratarg.or_else(|| match system_config.strategy {
404 "static" => Some(Strategy::Static(StaticStrategy::default())),
405 "adaptive" => Some(Strategy::Adaptive(AdaptiveStrategy::default())),
406 "conservative" => Some(Strategy::Conservative(ConservativeStrategy::default())),
407 _ => Some(Strategy::Conservative(ConservativeStrategy::default())),
408 });
409
410 let force_chunked = if args.chunked {
412 true
413 } else if args.no_chunked {
414 false
415 } else {
416 system_config.force_chunked
418 };
419
420 Ok(Self {
421 tailing: args.follow || args.sticky,
422 sticky: args.sticky,
423 offset,
424 offset_unit,
425 show_time: args.timestamps,
426 batch_window_ms: args.window,
427 mode,
428 force_chunked,
429 disable_chunked: args.no_chunked,
430 no_file_names: args.quiet,
431 all_file_names: args.verbose,
432 adaptive: args.adaptive,
433 strategy,
434 max_memory: Some(max_memory),
435 #[cfg(debug_assertions)]
436 conservative: args.conservative,
437 #[cfg(not(debug_assertions))]
438 conservative: false,
439 #[cfg(debug_assertions)]
440 profile_json: args.profile_json,
441 })
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn complicated_args() {
451 let args = crate::Args {
452 timestamps: true,
453 follow: true,
454 sticky: false,
455 blocks: None,
456 bytes: Some(-5),
457 offset: None,
458 verbose: false,
459 quiet: false,
460 window: 250,
461 chunked: false,
462 no_chunked: false,
463 args: vec!["-4".to_string()],
464 adaptive: false,
465 chunk_strategy: None,
466 max_memory: Some(10_000_000_000),
467 conservative: false,
468 #[cfg(debug_assertions)]
469 profile_json: false,
470 };
471 let config = ConfigOpts::new(&args).expect("Config should be valid for test");
472 assert_eq!(config.offset, -5);
473 assert!(matches!(config.mode, InputMode::Stdin));
474 }
475
476 #[test]
477 fn glob_expansions() {
478 let fixture_glob = "./fixtures/*.log".to_string();
479 let results = expand_globs(&[fixture_glob]).expect("this list of paths should expand successfully");
480 assert_eq!(results.len(), 8); assert_eq!(
482 results.as_slice(),
483 vec![
484 PathBuf::from("fixtures/ascii_colors.log"),
485 PathBuf::from("fixtures/garbage_prefix.log"),
486 PathBuf::from("fixtures/java_stacktrace.log"),
487 PathBuf::from("fixtures/just_loglines.log"),
488 PathBuf::from("fixtures/log4j.log"),
489 PathBuf::from("fixtures/mixed_json_types.log"),
490 PathBuf::from("fixtures/mixed_text_json.log"),
491 PathBuf::from("fixtures/windows_line_endings.log")
492 ]
493 );
494 }
495
496 #[test]
497 fn can_unescape_glob_pattern() {
498 assert_eq!(unescape_glob_pattern("\\*.log"), "*.log");
500 assert_eq!(unescape_glob_pattern("test\\?.txt"), "test?.txt");
501 assert_eq!(unescape_glob_pattern("\\[abc\\]"), "[abc]");
502
503 assert_eq!(unescape_glob_pattern("\\*.log\\?"), "*.log?");
505 assert_eq!(unescape_glob_pattern("test\\*file\\?.log"), "test*file?.log");
506
507 assert_eq!(unescape_glob_pattern("file\\name.txt"), "file\\name.txt");
509 assert_eq!(unescape_glob_pattern("path\\to\\file"), "path\\to\\file");
510
511 assert_eq!(unescape_glob_pattern("*.log"), "*.log");
513 assert_eq!(unescape_glob_pattern("test?.txt"), "test?.txt");
514
515 assert_eq!(unescape_glob_pattern(""), "");
517 assert_eq!(unescape_glob_pattern("\\"), "\\");
518 assert_eq!(unescape_glob_pattern("file\\"), "file\\");
519 }
520
521 #[test]
522 fn is_glob_with_escaped_patterns_works() {
523 assert!(is_glob("\\*.log"));
525 assert!(is_glob("test\\?.txt"));
526 assert!(is_glob("\\[abc]"));
527
528 assert!(is_glob("*.log"));
530 assert!(is_glob("test?.txt"));
531 assert!(is_glob("[abc]"));
532
533 assert!(!is_glob("file.log"));
535 assert!(!is_glob("test.txt"));
536 assert!(!is_glob("path/to/file"));
537
538 assert!(!is_glob("file\\name.txt"));
540 assert!(!is_glob("path\\to\\file"));
541 }
542
543 #[test]
544 fn can_expand_escaped_globs() {
545 let escaped_fixture_glob = ".\\*/fixtures/\\*.log".to_string();
548 let normal_fixture_glob = "./fixtures/*.log".to_string();
549
550 if let (Ok(escaped_results), Ok(normal_results)) = (
552 expand_globs(&[escaped_fixture_glob]),
553 expand_globs(&[normal_fixture_glob]),
554 ) {
555 assert_eq!(escaped_results, normal_results);
556 }
557 }
558
559 #[test]
560 fn can_modify_config() {
561 let initial_config = ConfigOpts {
562 tailing: false,
563 sticky: false,
564 offset: 10,
565 offset_unit: OffsetUnit::Lines,
566 show_time: false,
567 batch_window_ms: 250,
568 mode: InputMode::Stdin,
569 force_chunked: false,
570 disable_chunked: false,
571 ..Default::default()
572 };
573
574 with_config(initial_config.clone(), || {
576 update(|cfg| {
578 cfg.tailing = true;
579 cfg.offset = 20;
580 cfg.show_time = true;
581 });
582
583 assert!(tailing());
585 assert_eq!(offset(), 20);
586 assert!(show_time());
587 });
588 }
589
590 #[test]
591 fn can_test_with_config() {
592 let original_config = ConfigOpts::default();
593 set(original_config.clone()).expect("should set config");
594
595 let original_offset = offset();
596 let original_tailing = tailing();
597
598 let result = with_config(
600 ConfigOpts {
601 tailing: true,
602 sticky: false,
603 offset: 42,
604 offset_unit: OffsetUnit::Bytes,
605 show_time: true,
606 batch_window_ms: 500,
607 mode: InputMode::Stdin,
608 force_chunked: true,
609 disable_chunked: false,
610 ..Default::default()
611 },
612 || {
613 assert_eq!(offset(), 42);
615 assert!(tailing());
616 assert_eq!(batch_window_ms(), 500);
617 assert!(force_chunked());
618
619 "test_successful"
621 },
622 );
623
624 assert_eq!(offset(), original_offset);
626 assert_eq!(tailing(), original_tailing);
627 assert_eq!(result, "test_successful");
628 }
629
630 #[test]
631 fn concurrent_access_to_test_config() {
632 use std::thread;
633 use std::time::Duration;
634
635 let handles: Vec<_> = (0..3)
637 .map(|i| {
638 thread::spawn(move || {
639 let config = ConfigOpts {
640 offset: i * 10,
641 tailing: i % 2 == 0,
642 show_time: i % 2 == 1,
643 ..ConfigOpts::default()
644 };
645
646 set(config).expect("should set config");
647
648 thread::sleep(Duration::from_millis(10));
650
651 assert_eq!(offset(), i * 10);
653 assert_eq!(tailing(), i % 2 == 0);
654 assert_eq!(show_time(), i % 2 == 1);
655
656 i
657 })
658 })
659 .collect();
660
661 let results: Vec<_> = handles
663 .into_iter()
664 .map(|h| h.join().expect("test results should always be ok"))
665 .collect();
666 assert_eq!(results, vec![0, 1, 2]);
667 }
668
669 #[test]
670 fn config_accessors_work() {
671 let test_config = ConfigOpts {
672 tailing: true,
673 sticky: true,
674 offset: -100,
675 offset_unit: OffsetUnit::Blocks,
676 show_time: true,
677 batch_window_ms: 1000,
678 mode: InputMode::SingleFile {
679 path: PathBuf::from("test.log"),
680 },
681 force_chunked: true,
682 disable_chunked: false,
683 ..Default::default()
684 };
685
686 with_config(test_config, || {
688 assert!(tailing());
690 assert!(sticky());
691 assert_eq!(offset(), -100);
692 assert!(matches!(offset_unit(), OffsetUnit::Blocks));
693 assert!(show_time());
694 assert_eq!(batch_window_ms(), 1000);
695 assert!(matches!(mode(), InputMode::SingleFile { .. }));
696 assert!(force_chunked());
697 assert!(!disable_chunked());
698 });
699 }
700}