1use std::path::Path;
8use std::time::Duration as StdDuration;
9
10use time::format_description::well_known::Rfc3339;
11use time::OffsetDateTime;
12
13pub(crate) fn format_timestamp_rfc3339(value: OffsetDateTime) -> String {
16 value.format(&Rfc3339).unwrap_or_else(|_| value.to_string())
17}
18
19pub(crate) fn format_unix_ms_rfc3339(ms: i64) -> String {
21 let seconds = ms.div_euclid(1000);
22 let value = OffsetDateTime::from_unix_timestamp(seconds).unwrap_or(OffsetDateTime::UNIX_EPOCH);
23 format_timestamp_rfc3339(value)
24}
25
26pub(crate) fn format_duration_coarse(value: StdDuration) -> String {
31 if value.as_secs() == 0 {
32 return format!("{}ms", value.as_millis());
33 }
34 let seconds = value.as_secs();
35 if seconds < 60 {
36 return format!("{seconds}s");
37 }
38 if seconds < 60 * 60 {
39 return format!("{}m", seconds / 60);
40 }
41 if seconds < 60 * 60 * 24 {
42 return format!("{}h", seconds / (60 * 60));
43 }
44 if seconds < 60 * 60 * 24 * 7 {
45 return format!("{}d", seconds / (60 * 60 * 24));
46 }
47 if seconds.is_multiple_of(60 * 60 * 24 * 7) {
48 return format!("{}w", seconds / (60 * 60 * 24 * 7));
49 }
50 format!("{}d", seconds / (60 * 60 * 24))
51}
52
53pub(crate) fn shell_quote_path(path: &Path) -> String {
59 shell_words::quote(&path.to_string_lossy()).into_owned()
60}
61
62pub(crate) fn format_duration_ms(duration_ms: u64) -> String {
65 if duration_ms >= 60_000 {
66 format!("{:.1}m", duration_ms as f64 / 60_000.0)
67 } else if duration_ms >= 1_000 {
68 format!("{:.1}s", duration_ms as f64 / 1_000.0)
69 } else {
70 format!("{duration_ms}ms")
71 }
72}
73
74pub(crate) fn escape_md(value: &str) -> String {
79 value.replace('|', "\\|")
80}
81
82pub(crate) fn escape_html(value: &str) -> String {
87 let mut out = String::with_capacity(value.len());
88 for ch in value.chars() {
89 match ch {
90 '&' => out.push_str("&"),
91 '<' => out.push_str("<"),
92 '>' => out.push_str(">"),
93 '"' => out.push_str("""),
94 '\'' => out.push_str("'"),
95 _ => out.push(ch),
96 }
97 }
98 out
99}
100
101pub(crate) fn escape_toml_basic_string(value: &str) -> String {
107 let mut out = String::with_capacity(value.len());
108 for ch in value.chars() {
109 match ch {
110 '"' => out.push_str("\\\""),
111 '\\' => out.push_str("\\\\"),
112 '\n' => out.push_str("\\n"),
113 '\t' => out.push_str("\\t"),
114 '\r' => out.push_str("\\r"),
115 c if (c as u32) < 0x20 || c == '\u{7f}' => {
118 out.push_str(&format!("\\u{:04X}", c as u32));
119 }
120 _ => out.push(ch),
121 }
122 }
123 out
124}
125
126pub(crate) fn toml_basic_string_literal(value: &str) -> String {
128 format!("\"{}\"", escape_toml_basic_string(value))
129}
130
131pub(crate) fn slash_separators(value: &str) -> String {
134 value.replace('\\', "/")
135}
136
137pub(crate) fn slash_path(path: &Path) -> String {
139 slash_separators(&path.to_string_lossy())
140}
141
142pub(crate) fn looks_like_windows_drive_path(value: &str) -> bool {
145 let bytes = value.as_bytes();
146 bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn timestamp_uses_rfc3339() {
155 let value = OffsetDateTime::UNIX_EPOCH;
156 assert_eq!(format_timestamp_rfc3339(value), "1970-01-01T00:00:00Z");
157 }
158
159 #[test]
160 fn escape_html_handles_all_five_specials() {
161 assert_eq!(
162 escape_html(r#"<a href="x">&'b'</a>"#),
163 "<a href="x">&'b'</a>"
164 );
165 }
166
167 #[test]
168 fn escape_toml_basic_string_escapes_control_characters() {
169 assert_eq!(
170 escape_toml_basic_string("a\"b\\c\nd\te\rf"),
171 "a\\\"b\\\\c\\nd\\te\\rf"
172 );
173 assert_eq!(escape_toml_basic_string("bell\u{7}"), "bell\\u0007");
176 }
177
178 #[test]
179 fn toml_basic_string_literal_round_trips_windows_paths() {
180 let raw = r"C:\Users\RUNNER~1\AppData\Local\Temp\.tmpJHS6sR\coding-pack";
181 let parsed: toml::Value =
182 toml::from_str(&format!("path = {}\n", toml_basic_string_literal(raw))).unwrap();
183 assert_eq!(parsed.get("path").and_then(toml::Value::as_str), Some(raw));
184 }
185
186 #[test]
187 fn slash_path_normalizes_windows_separators() {
188 assert_eq!(
189 slash_separators(r"C:\tmp\pkg\lib.harn"),
190 "C:/tmp/pkg/lib.harn"
191 );
192 }
193
194 #[test]
195 fn detects_windows_drive_paths_without_url_parser() {
196 assert!(looks_like_windows_drive_path(r"C:\tmp\registry.toml"));
197 assert!(looks_like_windows_drive_path("D:/tmp/registry.toml"));
198 assert!(looks_like_windows_drive_path("E:relative"));
199 assert!(!looks_like_windows_drive_path(
200 "https://example.com/index.toml"
201 ));
202 assert!(!looks_like_windows_drive_path("/tmp/index.toml"));
203 }
204
205 #[test]
206 fn unix_ms_rounds_to_seconds() {
207 assert_eq!(format_unix_ms_rfc3339(0), "1970-01-01T00:00:00Z");
208 assert_eq!(format_unix_ms_rfc3339(1500), "1970-01-01T00:00:01Z");
209 assert_eq!(format_unix_ms_rfc3339(-1), "1969-12-31T23:59:59Z");
211 }
212
213 #[test]
214 fn coarse_duration_picks_a_unit() {
215 assert_eq!(format_duration_coarse(StdDuration::from_millis(0)), "0ms");
216 assert_eq!(format_duration_coarse(StdDuration::from_secs(5)), "5s");
217 assert_eq!(format_duration_coarse(StdDuration::from_mins(2)), "2m");
218 assert_eq!(format_duration_coarse(StdDuration::from_hours(2)), "2h");
219 assert_eq!(format_duration_coarse(StdDuration::from_hours(72)), "3d");
220 assert_eq!(format_duration_coarse(StdDuration::from_hours(336)), "2w");
221 }
222
223 #[test]
224 fn ms_duration_uses_one_decimal_above_a_second() {
225 assert_eq!(format_duration_ms(500), "500ms");
226 assert_eq!(format_duration_ms(1_500), "1.5s");
227 assert_eq!(format_duration_ms(90_000), "1.5m");
228 }
229
230 #[test]
231 fn escape_md_escapes_only_pipes() {
232 assert_eq!(escape_md("a|b|c"), "a\\|b\\|c");
233 assert_eq!(escape_md("no pipes here"), "no pipes here");
234 assert_eq!(escape_md(""), "");
235 }
236}