1use crate::frame::{CAN_FLAG_EXTENDED, CAN_FLAG_FD, CanFrame};
2use crate::protocol::EventDirection;
3use std::io::Write;
4use std::path::{Path, PathBuf};
5use std::time::{Instant, SystemTime, UNIX_EPOCH};
6
7pub struct TraceWriter {
8 path: PathBuf,
9 started_at: Instant,
10 writer: std::io::BufWriter<std::fs::File>,
11}
12
13impl TraceWriter {
14 pub fn start(path: &Path) -> Result<Self, String> {
15 Self::start_with_clock(path, SystemTime::now(), Instant::now())
16 }
17
18 fn start_with_clock(
19 path: &Path,
20 started_at_wall_clock: SystemTime,
21 started_at_monotonic: Instant,
22 ) -> Result<Self, String> {
23 let path = normalize_trace_path(path)?;
24 let file = std::fs::OpenOptions::new()
25 .write(true)
26 .create_new(true)
27 .open(&path)
28 .map_err(|err| format!("failed to create trace '{}': {err}", path.display()))?;
29 let mut writer = std::io::BufWriter::new(file);
30 writeln!(
31 writer,
32 "date {}",
33 format_trace_header_date(started_at_wall_clock)?
34 )
35 .map_err(|err| format!("failed to initialize trace '{}': {err}", path.display()))?;
36 writeln!(writer, "base hex timestamps absolute")
37 .map_err(|err| format!("failed to initialize trace '{}': {err}", path.display()))?;
38 Ok(Self {
39 path,
40 started_at: started_at_monotonic,
41 writer,
42 })
43 }
44
45 pub fn path(&self) -> &Path {
46 &self.path
47 }
48
49 pub fn write_event(
50 &mut self,
51 direction: EventDirection,
52 frame: &CanFrame,
53 ) -> Result<(), String> {
54 let seconds = self.started_at.elapsed().as_secs_f64();
55 let direction = match direction {
56 EventDirection::Rx => "Rx",
57 EventDirection::Tx => "Tx",
58 };
59 let id = if (frame.flags & CAN_FLAG_EXTENDED) != 0 {
60 format!("{:08X}x", frame.arb_id)
61 } else {
62 format!("{:03X}", frame.arb_id)
63 };
64 let payload = crate::protocol::payload_to_hex(frame.payload());
65 let fd_suffix = if (frame.flags & CAN_FLAG_FD) != 0 {
66 " FD"
67 } else {
68 ""
69 };
70 writeln!(
71 self.writer,
72 "{seconds:>12.6} {direction} {id} {payload}{fd_suffix}"
73 )
74 .map_err(|err| format!("failed to write trace '{}': {err}", self.path.display()))
75 }
76
77 pub fn finish(mut self) -> Result<PathBuf, String> {
78 self.writer
79 .flush()
80 .map_err(|err| format!("failed to flush trace '{}': {err}", self.path.display()))?;
81 Ok(self.path)
82 }
83}
84
85fn normalize_trace_path(raw_path: &Path) -> Result<PathBuf, String> {
86 if !raw_path.is_absolute() {
87 return Err(format!(
88 "trace path '{}' must be absolute",
89 raw_path.display()
90 ));
91 }
92 let file_name = raw_path.file_name().ok_or_else(|| {
93 format!(
94 "trace path '{}' must include a file name",
95 raw_path.display()
96 )
97 })?;
98 let parent = raw_path.parent().ok_or_else(|| {
99 format!(
100 "trace path '{}' must include a parent directory",
101 raw_path.display()
102 )
103 })?;
104 std::fs::create_dir_all(parent).map_err(|err| {
105 format!(
106 "failed to create trace directory '{}': {err}",
107 parent.display()
108 )
109 })?;
110 let canonical_parent = std::fs::canonicalize(parent).map_err(|err| {
111 format!(
112 "failed to resolve trace directory '{}': {err}",
113 parent.display()
114 )
115 })?;
116 let normalized = canonical_parent.join(file_name);
117 if let Ok(metadata) = std::fs::symlink_metadata(&normalized) {
118 if metadata.file_type().is_symlink() {
119 return Err(format!(
120 "refusing to write trace through symlink '{}'",
121 normalized.display()
122 ));
123 }
124 if metadata.is_dir() {
125 return Err(format!(
126 "trace path '{}' points to a directory",
127 normalized.display()
128 ));
129 }
130 }
131 Ok(normalized)
132}
133
134fn format_trace_header_date(at: SystemTime) -> Result<String, String> {
135 let duration = at
136 .duration_since(UNIX_EPOCH)
137 .map_err(|err| format!("trace clock is before unix epoch: {err}"))?;
138 let total_seconds = duration.as_secs();
139 let millis = duration.subsec_millis();
140 let days = i64::try_from(total_seconds / 86_400)
141 .map_err(|_| "trace timestamp day count overflowed".to_string())?;
142 let seconds_of_day = total_seconds % 86_400;
143 let (year, month, day) = civil_from_days(days);
144 let hour = seconds_of_day / 3_600;
145 let minute = (seconds_of_day % 3_600) / 60;
146 let second = seconds_of_day % 60;
147 Ok(format!(
148 "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}.{millis:03}"
149 ))
150}
151
152fn civil_from_days(days_since_unix_epoch: i64) -> (i32, u32, u32) {
153 let z = days_since_unix_epoch + 719_468;
154 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
155 let doe = z - era * 146_097;
156 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
157 let y = yoe + era * 400;
158 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
159 let mp = (5 * doy + 2) / 153;
160 let day = doy - (153 * mp + 2) / 5 + 1;
161 let month = mp + if mp < 10 { 3 } else { -9 };
162 let year = y + if month <= 2 { 1 } else { 0 };
163 (year as i32, month as u32, day as u32)
164}
165
166#[cfg(test)]
167mod tests {
168 use super::{TraceWriter, format_trace_header_date};
169 use crate::frame::CanFrame;
170 use crate::protocol::EventDirection;
171 use std::path::Path;
172 use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
173
174 #[test]
175 fn trace_header_date_formats_real_timestamp() {
176 let at = UNIX_EPOCH + Duration::from_millis(1_712_925_296_789);
177 assert_eq!(
178 format_trace_header_date(at).expect("format"),
179 "2024-04-12 12:34:56.789"
180 );
181 }
182
183 #[test]
184 fn trace_start_writes_header_with_resolved_absolute_path() {
185 let tempdir = tempfile::tempdir().expect("tempdir");
186 let path = tempdir.path().join("subdir").join("session.asc");
187 let started_at = UNIX_EPOCH + Duration::from_millis(1_712_925_296_789);
188
189 let writer =
190 TraceWriter::start_with_clock(&path, started_at, Instant::now()).expect("start trace");
191 let path = writer.path().to_path_buf();
192 let finished_path = writer.finish().expect("finish trace");
193
194 assert_eq!(finished_path, path);
195 assert!(path.is_absolute());
196
197 let contents = std::fs::read_to_string(&path).expect("read trace");
198 let lines = contents.lines().collect::<Vec<_>>();
199 assert_eq!(lines[0], "date 2024-04-12 12:34:56.789");
200 assert_eq!(lines[1], "base hex timestamps absolute");
201 }
202
203 #[test]
204 fn trace_start_rejects_relative_paths() {
205 let err = match TraceWriter::start(Path::new("relative.asc")) {
206 Ok(_) => panic!("relative path must fail"),
207 Err(err) => err,
208 };
209 assert!(err.contains("must be absolute"));
210 }
211
212 #[cfg(unix)]
213 #[test]
214 fn trace_start_rejects_symlink_targets() {
215 let tempdir = tempfile::tempdir().expect("tempdir");
216 let real_target = tempdir.path().join("real.asc");
217 std::fs::write(&real_target, "").expect("seed target");
218 let symlink_path = tempdir.path().join("link.asc");
219 std::os::unix::fs::symlink(&real_target, &symlink_path).expect("symlink");
220
221 let err = match TraceWriter::start(&symlink_path) {
222 Ok(_) => panic!("symlink must fail"),
223 Err(err) => err,
224 };
225 assert!(err.contains("refusing to write trace through symlink"));
226 }
227
228 #[test]
229 fn trace_writer_records_ascii_event_lines() {
230 let tempdir = tempfile::tempdir().expect("tempdir");
231 let path = tempdir.path().join("events.asc");
232 let started_at = SystemTime::now();
233 let mut writer =
234 TraceWriter::start_with_clock(&path, started_at, Instant::now()).expect("start");
235 writer
236 .write_event(
237 EventDirection::Tx,
238 &CanFrame {
239 arb_id: 0x123,
240 len: 2,
241 flags: 0,
242 data: [
243 0xAB, 0xCD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
244 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
245 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
246 ],
247 },
248 )
249 .expect("write event");
250 writer.finish().expect("finish");
251
252 let contents = std::fs::read_to_string(&path).expect("read");
253 assert!(
254 contents
255 .lines()
256 .nth(2)
257 .expect("event line")
258 .contains("Tx 123 ABCD")
259 );
260 }
261}