1use std::collections::VecDeque;
7
8use crate::format::Format;
9use crate::types::{
10 BlockingConfig, Conversion, ConversionConfig, ConversionKind, ConversionPriority,
11 ConversionStep, CoreValue, PriorityConfig,
12};
13
14const MAX_BFS_DEPTH: usize = 5;
16
17const UNIT_FORMATS: &[&str] = &[
19 "length",
20 "weight",
21 "volume",
22 "speed",
23 "pressure",
24 "angle",
25 "area",
26 "energy",
27 "temperature",
28];
29
30const UNIT_TARGETS: &[&str] = &[
32 "meters",
34 "kilometers",
35 "centimeters",
36 "millimeters",
37 "feet",
38 "miles",
39 "inches",
40 "grams",
42 "kilograms",
43 "milligrams",
44 "pounds",
45 "ounces",
46 "milliliters",
48 "liters",
49 "gallons",
50 "fluid ounces",
51 "cups",
52 "m/s",
54 "km/h",
55 "mph",
56 "knots",
57 "pascals",
59 "kilopascals",
60 "megapascals",
61 "bar",
62 "psi",
63 "atmospheres",
64 "degrees",
66 "radians",
67 "gradians",
68 "turns",
69 "square meters",
71 "square kilometers",
72 "square centimeters",
73 "square feet",
74 "acres",
75 "hectares",
76 "joules",
78 "kilojoules",
79 "megajoules",
80 "calories",
81 "kilocalories",
82 "kilowatt-hours",
83 "celsius",
85 "fahrenheit",
86 "kelvin",
87];
88
89const ROOT_BLOCKED_TARGETS: &[(&str, &str)] = &[
93 ("text", "ipv4"),
96 ("text", "ipv6"),
97 ("text", "color-rgb"),
99 ("text", "color-hex"),
100 ("text", "color-hsl"),
101 ("text", "int-be"),
104 ("text", "int-le"),
105 ("text", "epoch-seconds"),
106 ("text", "epoch-millis"),
107 ("text", "apple-cocoa"),
108 ("text", "filetime"),
109 ("text", "duration"),
110 ("text", "duration-ms"),
111 ("text", "datasize"),
112 ("text", "datasize-iec"),
113 ("text", "datasize-si"),
114 ("text", "uuid"),
117 ("hex", "ipv4"),
120 ("hex", "ipv6"),
121 ("hex", "ip"),
122 ("hex", "color-rgb"),
125 ("hex", "color-hsl"),
126];
127
128const BLOCKED_PATHS: &[(&str, &str)] = &[
131 ("ipv4", "msgpack"),
133 ("ipv6", "msgpack"),
134 ("uuid", "msgpack"),
136 ("ipv4", "epoch-seconds"),
138 ("ipv4", "epoch-millis"),
139 ("ipv4", "apple-cocoa"),
140 ("ipv4", "filetime"),
141 ("uuid", "epoch-seconds"),
143 ("uuid", "epoch-millis"),
144 ("uuid", "apple-cocoa"),
145 ("uuid", "filetime"),
146 ("expr", "msgpack"),
148 ("expr", "octal"),
149 ("expr", "datasize"),
150 ("expr", "datasize-iec"),
151 ("expr", "datasize-si"),
152 ("expr", "duration"),
153 ("expr", "duration-ms"),
154 ("expr", "decimal"),
155 ("datasize", "duration"),
157 ("datasize", "duration-ms"),
158 ("duration", "datasize"),
160 ("duration", "datasize-iec"),
161 ("duration", "datasize-si"),
162 ("duration", "duration-ms"),
163 ("color-hex", "duration"),
165 ("color-hex", "duration-ms"),
166 ("color-hex", "datasize"),
167 ("color-hex", "datasize-iec"),
168 ("color-hex", "datasize-si"),
169 ("color-rgb", "duration"),
170 ("color-rgb", "duration-ms"),
171 ("color-rgb", "datasize"),
172 ("color-rgb", "datasize-iec"),
173 ("color-rgb", "datasize-si"),
174 ("color-hsl", "duration"),
175 ("color-hsl", "duration-ms"),
176 ("color-hsl", "datasize"),
177 ("color-hsl", "datasize-iec"),
178 ("color-hsl", "datasize-si"),
179 ("hexdump", "bytes"),
181 ("hexdump", "url-encoded"),
182 ("hexdump", "escape-unicode"),
183 ("hexdump", "escape-hex"),
184 ("hexdump", "msgpack"),
185 ("url-encoded", "url-encoded"),
187 ("url-encoded", "bytes"),
188 ("url-encoded", "escape-unicode"),
189 ("url-encoded", "escape-hex"),
190 ("text", "url-encoded"),
192 ("text", "graph"),
193 ("text", "text"),
194 ("text", "msgpack"),
195 ("text", "escape-unicode"),
196 ("escape-hex", "bytes"),
198 ("escape-hex", "url-encoded"),
199 ("escape-unicode", "bytes"),
200 ("escape-unicode", "url-encoded"),
201 ("text", "int-be"),
203 ("text", "int-le"),
204 ("text", "utf8"),
206 ("text", "ipv4"),
209 ("text", "ipv6"),
210 ("text", "color-rgb"),
211 ("text", "color-hex"),
212 ("text", "color-hsl"),
213];
214
215fn is_blocked_path_builtin(source_format: &str, target_format: &str) -> bool {
217 if BLOCKED_PATHS
219 .iter()
220 .any(|(src, tgt)| source_format == *src && target_format == *tgt)
221 {
222 return true;
223 }
224
225 if UNIT_FORMATS.contains(&source_format) && UNIT_TARGETS.contains(&target_format) {
228 let source_owns_target = match source_format {
231 "length" => matches!(
232 target_format,
233 "meters"
234 | "kilometers"
235 | "centimeters"
236 | "millimeters"
237 | "feet"
238 | "miles"
239 | "inches"
240 ),
241 "weight" => matches!(
242 target_format,
243 "grams" | "kilograms" | "milligrams" | "pounds" | "ounces"
244 ),
245 "volume" => matches!(
246 target_format,
247 "milliliters" | "liters" | "gallons" | "fluid ounces" | "cups"
248 ),
249 "speed" => matches!(target_format, "m/s" | "km/h" | "mph" | "knots"),
250 "pressure" => matches!(
251 target_format,
252 "pascals" | "kilopascals" | "megapascals" | "bar" | "psi" | "atmospheres"
253 ),
254 "angle" => matches!(target_format, "degrees" | "radians" | "gradians" | "turns"),
255 "area" => matches!(
256 target_format,
257 "square meters"
258 | "square kilometers"
259 | "square centimeters"
260 | "square feet"
261 | "acres"
262 | "hectares"
263 ),
264 "energy" => matches!(
265 target_format,
266 "joules"
267 | "kilojoules"
268 | "megajoules"
269 | "calories"
270 | "kilocalories"
271 | "kilowatt-hours"
272 ),
273 "temperature" => matches!(target_format, "celsius" | "fahrenheit" | "kelvin"),
274 _ => false,
275 };
276 if !source_owns_target {
277 return true;
278 }
279 }
280
281 false
282}
283
284fn is_root_blocked_builtin(root_format: &str, target_format: &str) -> bool {
286 ROOT_BLOCKED_TARGETS
287 .iter()
288 .any(|(root, target)| root_format == *root && target_format == *target)
289}
290
291fn is_blocked(
293 source_format: &str,
294 target_format: &str,
295 root_format: Option<&str>,
296 path: &[String],
297 blocking: Option<&BlockingConfig>,
298) -> bool {
299 if is_blocked_path_builtin(source_format, target_format) {
301 return true;
302 }
303
304 if let Some(root) = root_format {
306 if is_root_blocked_builtin(root, target_format) {
307 return true;
308 }
309 }
310
311 if let Some(config) = blocking {
313 if config.is_format_blocked(target_format) {
315 return true;
316 }
317 if config.is_path_blocked(path) {
319 return true;
320 }
321 if let Some(root) = root_format {
323 if config.is_root_blocked(root, target_format) {
324 return true;
325 }
326 }
327 }
328
329 false
330}
331
332pub fn find_all_conversions(
340 formats: &[Box<dyn Format>],
341 initial: &CoreValue,
342 exclude_format: Option<&str>,
343 source_format: Option<&str>,
344 config: Option<&ConversionConfig>,
345) -> Vec<Conversion> {
346 let blocking = config.map(|c| &c.blocking);
347 let priority = config.map(|c| &c.priority);
348 let mut results = Vec::new();
349 let mut seen_results: std::collections::HashSet<(String, String)> =
352 std::collections::HashSet::new();
353 let mut seen_for_bfs: std::collections::HashSet<(String, String)> =
357 std::collections::HashSet::new();
358
359 if let Some(excluded) = exclude_format {
361 seen_results.insert((excluded.to_string(), String::new()));
363 }
364
365 let mut queue: VecDeque<(CoreValue, Vec<String>, Vec<ConversionStep>)> = VecDeque::new();
367
368 let initial_path = source_format
370 .map(|s| vec![s.to_string()])
371 .unwrap_or_default();
372 queue.push_back((initial.clone(), initial_path, vec![]));
373
374 if let Some(source_fmt) = source_format {
378 if let Some(format) = formats.iter().find(|f| f.id() == source_fmt) {
379 for mut conv in format.source_conversions(initial) {
380 let mut path = vec![source_fmt.to_string()];
382 path.push(conv.target_format.clone());
383 conv.path = path;
384
385 conv.steps = vec![ConversionStep {
387 format: conv.target_format.clone(),
388 value: conv.value.clone(),
389 display: conv.display.clone(),
390 }];
391
392 let result_key = (conv.target_format.clone(), conv.display.clone());
393 if seen_results.insert(result_key) {
394 results.push(conv);
395 }
396 }
397 }
398 }
399
400 for format in formats {
402 if format.can_format(initial) {
403 if let Some(display) = format.format(initial) {
404 let format_id = format.id().to_string();
405 let result_key = (format_id.clone(), display.clone());
406 if seen_results.insert(result_key) {
407 let mut path = source_format
409 .map(|s| vec![s.to_string()])
410 .unwrap_or_default();
411 path.push(format_id.clone());
412
413 results.push(Conversion {
414 value: initial.clone(),
415 target_format: format_id.clone(),
416 display: display.clone(),
417 path,
418 steps: vec![ConversionStep {
419 format: format_id,
420 value: initial.clone(),
421 display,
422 }],
423 is_lossy: false,
424 priority: ConversionPriority::default(),
425 display_only: false,
426 kind: ConversionKind::default(),
427 hidden: false,
428 rich_display: vec![],
429 });
430 }
431 }
432 }
433 }
434
435 let reinterpret_threshold = config.map(|c| c.reinterpret_threshold()).unwrap_or(0.7);
437
438 let mut depth = 0;
440
441 while !queue.is_empty() && depth < MAX_BFS_DEPTH {
442 let level_size = queue.len();
443
444 for _ in 0..level_size {
445 let Some((current_value, current_path, current_steps)) = queue.pop_front() else {
446 break;
447 };
448
449 let immediate_source = current_path.last().map(|s| s.as_str()).unwrap_or("");
451
452 if let CoreValue::String(s) = ¤t_value {
455 if !current_path.is_empty() && reinterpret_threshold < 1.0 {
458 for format in formats {
459 if format.id() == "text" {
461 continue;
462 }
463
464 for interp in format.parse(s) {
465 if interp.confidence < reinterpret_threshold {
467 continue;
468 }
469
470 let target_format = interp.source_format.clone();
471
472 if is_blocked(
476 immediate_source,
477 &target_format,
478 None, ¤t_path,
480 blocking,
481 ) {
482 continue;
483 }
484
485 let display = format
487 .format(&interp.value)
488 .unwrap_or_else(|| interp.description.clone());
489
490 let result_key = (target_format.clone(), display.clone());
491 let bfs_key = (target_format.clone(), display.clone());
492
493 let mut full_path = current_path.clone();
495 full_path.push(target_format.clone());
496
497 let mut full_steps = current_steps.clone();
499 full_steps.push(ConversionStep {
500 format: target_format.clone(),
501 value: interp.value.clone(),
502 display: display.clone(),
503 });
504
505 if seen_results.insert(result_key) {
507 results.push(Conversion {
508 value: interp.value.clone(),
509 target_format: target_format.clone(),
510 display: display.clone(),
511 path: full_path.clone(),
512 steps: full_steps.clone(),
513 is_lossy: false,
514 priority: ConversionPriority::Structured,
515 kind: ConversionKind::Conversion,
516 display_only: false,
517 hidden: false,
518 rich_display: interp.rich_display.clone(),
519 });
520 }
521
522 if seen_for_bfs.insert(bfs_key) {
524 queue.push_back((interp.value, full_path, full_steps));
525 }
526 }
527 }
528 }
529 }
530
531 for format in formats {
533 for conv in format.conversions(¤t_value) {
534 if is_blocked(
536 immediate_source,
537 &conv.target_format,
538 source_format,
539 ¤t_path,
540 blocking,
541 ) {
542 continue;
543 }
544
545 let result_key = (conv.target_format.clone(), conv.display.clone());
546 let bfs_key = (conv.target_format.clone(), conv.display.clone());
547
548 let mut full_path = current_path.clone();
550 full_path.extend(conv.path.clone());
551
552 let mut full_steps = current_steps.clone();
554 for step in &conv.steps {
556 full_steps.push(step.clone());
557 }
558 if full_steps.is_empty()
560 || full_steps.last().map(|s| &s.format) != Some(&conv.target_format)
561 {
562 full_steps.push(ConversionStep {
563 format: conv.target_format.clone(),
564 value: conv.value.clone(),
565 display: conv.display.clone(),
566 });
567 }
568
569 if seen_results.insert(result_key) {
571 results.push(Conversion {
572 value: conv.value.clone(),
573 target_format: conv.target_format.clone(),
574 display: conv.display.clone(),
575 path: full_path.clone(),
576 steps: full_steps.clone(),
577 is_lossy: conv.is_lossy,
578 priority: conv.priority,
579 kind: conv.kind,
580 display_only: conv.display_only,
581 hidden: conv.hidden,
582 rich_display: conv.rich_display.clone(),
583 });
584 }
585
586 if !conv.display_only && seen_for_bfs.insert(bfs_key) {
588 queue.push_back((conv.value, full_path, full_steps));
589 }
590 }
591 }
592 }
593
594 depth += 1;
595 }
596
597 if let Some(source) = exclude_format {
600 results.retain(|conv| {
601 !is_blocked(
602 source,
603 &conv.target_format,
604 source_format,
605 &conv.path,
606 blocking,
607 )
608 });
609 }
610
611 sort_conversions(&mut results, priority);
613
614 results
615}
616
617fn sort_conversions(results: &mut [Conversion], priority_config: Option<&PriorityConfig>) {
619 results.sort_by(|a, b| {
620 if let Some(config) = priority_config {
621 let cat_a = config.category_sort_key(a.priority);
623 let cat_b = config.category_sort_key(b.priority);
624
625 if cat_a == cat_b {
627 let off_a = config.format_offset(&a.target_format);
629 let off_b = config.format_offset(&b.target_format);
630 off_b
632 .cmp(&off_a)
633 .then_with(|| a.path.len().cmp(&b.path.len()))
634 } else {
635 cat_a.cmp(&cat_b)
636 }
637 } else {
638 a.priority
640 .cmp(&b.priority)
641 .then_with(|| a.path.len().cmp(&b.path.len()))
642 }
643 });
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use crate::formats::{Base64Format, BytesToIntFormat, DateTimeFormat, HexFormat};
650
651 #[test]
652 fn test_bytes_to_multiple_formats() {
653 let formats: Vec<Box<dyn Format>> = vec![
654 Box::new(HexFormat),
655 Box::new(Base64Format),
656 Box::new(BytesToIntFormat),
657 ];
658
659 let bytes = CoreValue::Bytes(vec![0x69, 0x1E, 0x01, 0xB8]);
660 let conversions = find_all_conversions(&formats, &bytes, None, None, None);
661
662 let format_ids: Vec<_> = conversions
664 .iter()
665 .map(|c| c.target_format.as_str())
666 .collect();
667
668 assert!(format_ids.contains(&"hex"));
669 assert!(format_ids.contains(&"base64"));
670 assert!(format_ids.contains(&"int-be"));
671 assert!(format_ids.contains(&"int-le"));
672 }
673
674 #[test]
675 fn test_int_to_datetime() {
676 let formats: Vec<Box<dyn Format>> = vec![Box::new(DateTimeFormat)];
677
678 let value = CoreValue::Int {
679 value: 1763574200,
680 original_bytes: None,
681 };
682
683 let conversions = find_all_conversions(&formats, &value, None, None, None);
684
685 let datetime_conv = conversions
686 .iter()
687 .find(|c| c.target_format == "epoch-seconds");
688 assert!(datetime_conv.is_some());
689 assert!(datetime_conv.unwrap().display.contains("2025"));
690 }
691
692 #[test]
693 fn test_chained_conversions() {
694 let formats: Vec<Box<dyn Format>> = vec![
695 Box::new(HexFormat),
696 Box::new(BytesToIntFormat),
697 Box::new(DateTimeFormat),
698 ];
699
700 let bytes = CoreValue::Bytes(vec![0x69, 0x1E, 0x01, 0xB8]);
702 let conversions = find_all_conversions(&formats, &bytes, None, None, None);
703
704 let datetime_conv = conversions
706 .iter()
707 .find(|c| c.target_format == "epoch-seconds");
708
709 assert!(
710 datetime_conv.is_some(),
711 "Should find epoch-seconds conversion"
712 );
713 let dt = datetime_conv.unwrap();
714 assert!(dt.display.contains("2025"));
715 assert!(!dt.path.is_empty()); }
717}