Skip to main content

steam_user/utils/
debug.rs

1use std::{
2    env, fs,
3    path::Path,
4    time::{SystemTime, UNIX_EPOCH},
5};
6
7/// Dumps the provided HTML to a file if the `STEAM_USER_DEBUG_HTML` environment
8/// variable is set.
9///
10/// The filename will be formatted as `debug_html_{context}_{timestamp}.html`.
11///
12/// # Arguments
13///
14/// * `context` - A string identifier for where the dump is happening (e.g.,
15///   "friends_list").
16/// * `html` - The HTML content to dump.
17pub fn dump_html(context: &str, html: &str) {
18    if env::var("STEAM_USER_DEBUG_HTML").is_err() {
19        return;
20    }
21
22    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
23
24    let filename = format!("debug_html_{}_{}.html", context, timestamp);
25    let path = Path::new(&filename);
26
27    // Attempt to write to a 'debug_dumps' directory if possible, otherwise
28    // current dir. `create_dir_all` is used instead of
29    // `if !exists { create_dir }` to eliminate the TOCTOU race: the directory
30    // may be created by another process/thread between the existence check and
31    // the creation call. `create_dir_all` treats `AlreadyExists` as success.
32    let dump_dir = Path::new("debug_dumps");
33    let final_path = match fs::create_dir_all(dump_dir) {
34        Ok(_) => dump_dir.join(&filename),
35        Err(_) => path.to_path_buf(),
36    };
37
38    if let Err(e) = fs::write(&final_path, html) {
39        tracing::error!("Failed to dump debug HTML to {:?}: {}", final_path, e);
40    } else {
41        tracing::debug!("Debug HTML dumped to {:?}", final_path);
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use std::fs;
48
49    use super::*;
50
51    #[test]
52    #[serial_test::serial]
53    fn test_dump_html_creates_file() {
54        // SAFETY: this is a single-threaded test (enforced by #[serial_test::serial]);
55        // no other threads concurrently read or write the process environment,
56        // which is the precondition `set_var`/`remove_var` require since Rust 1.79.
57        unsafe {
58            env::set_var("STEAM_USER_DEBUG_HTML", "1");
59        }
60
61        let context = "test_context";
62        let html = "<html><body>Test</body></html>";
63
64        dump_html(context, html);
65
66        // Verify a file was created
67        let dump_dir = Path::new("debug_dumps");
68        let exists = if dump_dir.exists() {
69            fs::read_dir(dump_dir).unwrap().any(|entry| {
70                let entry = entry.unwrap();
71                let name = entry.file_name().into_string().unwrap();
72                name.starts_with("debug_html_test_context")
73            })
74        } else {
75            fs::read_dir(".").unwrap().any(|entry| {
76                let entry = entry.unwrap();
77                let name = entry.file_name().into_string().unwrap();
78                name.starts_with("debug_html_test_context")
79            })
80        };
81
82        assert!(exists, "Debug HTML file was not created");
83
84        // SAFETY: same as above.
85        unsafe {
86            env::remove_var("STEAM_USER_DEBUG_HTML");
87        }
88
89        if dump_dir.exists() {
90            for entry in fs::read_dir(dump_dir).unwrap() {
91                let entry = entry.unwrap();
92                let path = entry.path();
93                let name = entry.file_name().into_string().unwrap();
94                if name.starts_with("debug_html_test_context") {
95                    let _ = fs::remove_file(path);
96                }
97            }
98        }
99    }
100}