1use crate::{HistoryEntry, Shell};
10
11#[must_use]
14pub fn unescape(s: &str) -> String {
15 let mut out = String::with_capacity(s.len());
16 let mut chars = s.chars();
17 while let Some(c) = chars.next() {
18 if c == '\\' {
19 match chars.next() {
20 Some('\\') | None => out.push('\\'),
23 Some('n') => out.push('\n'),
24 Some(other) => {
25 out.push('\\');
26 out.push(other);
27 }
28 }
29 } else {
30 out.push(c);
31 }
32 }
33 out
34}
35
36#[must_use]
38pub fn parse(data: &[u8]) -> Vec<HistoryEntry> {
39 let text = String::from_utf8_lossy(crate::strip_bom(data));
40 let mut entries: Vec<HistoryEntry> = Vec::new();
41 let mut in_paths = false;
42
43 for line in text.lines() {
44 if let Some(cmd) = line.strip_prefix("- cmd: ") {
45 entries.push(HistoryEntry {
46 shell: Shell::Fish,
47 command: unescape(cmd),
48 timestamp: None,
49 elapsed: None,
50 paths: Vec::new(),
51 });
52 in_paths = false;
53 } else if let Some(when) = line.strip_prefix(" when: ") {
54 if let Some(last) = entries.last_mut() {
55 last.timestamp = when.trim().parse::<i64>().ok();
56 }
57 in_paths = false;
58 } else if line.trim_end() == " paths:" {
59 in_paths = true;
60 } else if in_paths {
61 if let Some(path) = line.strip_prefix(" - ") {
62 if let Some(last) = entries.last_mut() {
63 last.paths.push(unescape(path));
64 }
65 } else {
66 in_paths = false;
67 }
68 }
69 }
70 entries
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn unescape_reverses_only_backslash_and_newline() {
79 assert_eq!(unescape(r"a\\b"), r"a\b");
80 assert_eq!(unescape(r"line1\nline2"), "line1\nline2");
81 assert_eq!(unescape(r"keep\:colon"), r"keep\:colon"); }
83
84 #[test]
85 fn parses_cmd_when_and_paths() {
86 let data = b"- cmd: git status\n when: 1700000000\n paths:\n - /repo\n- cmd: ls\n when: 1700000005\n";
87 let e = parse(data);
88 assert_eq!(e.len(), 2);
89 assert_eq!(e[0].command, "git status");
90 assert_eq!(e[0].timestamp, Some(1_700_000_000));
91 assert_eq!(e[0].paths, vec!["/repo".to_string()]);
92 assert_eq!(e[1].command, "ls");
93 assert!(e[1].paths.is_empty());
94 }
95
96 #[test]
97 fn cmd_with_colon_and_hash_is_kept_verbatim() {
98 let e = parse(b"- cmd: echo http://x # note\n when: 1700000000\n");
100 assert_eq!(e.len(), 1);
101 assert_eq!(e[0].command, "echo http://x # note");
102 }
103
104 #[test]
105 fn escaped_newline_in_cmd_is_restored() {
106 let e = parse(b"- cmd: echo a\\nb\n when: 1700000000\n");
107 assert_eq!(e[0].command, "echo a\nb");
108 }
109
110 #[test]
111 fn empty_input_yields_no_entries() {
112 assert!(parse(b"").is_empty());
113 }
114}