steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
use std::{
    env, fs,
    path::Path,
    time::{SystemTime, UNIX_EPOCH},
};

/// Dumps the provided HTML to a file if the `STEAM_USER_DEBUG_HTML` environment
/// variable is set.
///
/// The filename will be formatted as `debug_html_{context}_{timestamp}.html`.
///
/// # Arguments
///
/// * `context` - A string identifier for where the dump is happening (e.g.,
///   "friends_list").
/// * `html` - The HTML content to dump.
pub fn dump_html(context: &str, html: &str) {
    if env::var("STEAM_USER_DEBUG_HTML").is_err() {
        return;
    }

    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();

    let filename = format!("debug_html_{}_{}.html", context, timestamp);
    let path = Path::new(&filename);

    // Attempt to write to a 'debug_dumps' directory if possible, otherwise
    // current dir. `create_dir_all` is used instead of
    // `if !exists { create_dir }` to eliminate the TOCTOU race: the directory
    // may be created by another process/thread between the existence check and
    // the creation call. `create_dir_all` treats `AlreadyExists` as success.
    let dump_dir = Path::new("debug_dumps");
    let final_path = match fs::create_dir_all(dump_dir) {
        Ok(_) => dump_dir.join(&filename),
        Err(_) => path.to_path_buf(),
    };

    if let Err(e) = fs::write(&final_path, html) {
        tracing::error!("Failed to dump debug HTML to {:?}: {}", final_path, e);
    } else {
        tracing::debug!("Debug HTML dumped to {:?}", final_path);
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use super::*;

    #[test]
    #[serial_test::serial]
    fn test_dump_html_creates_file() {
        // SAFETY: this is a single-threaded test (enforced by #[serial_test::serial]);
        // no other threads concurrently read or write the process environment,
        // which is the precondition `set_var`/`remove_var` require since Rust 1.79.
        unsafe {
            env::set_var("STEAM_USER_DEBUG_HTML", "1");
        }

        let context = "test_context";
        let html = "<html><body>Test</body></html>";

        dump_html(context, html);

        // Verify a file was created
        let dump_dir = Path::new("debug_dumps");
        let exists = if dump_dir.exists() {
            fs::read_dir(dump_dir).unwrap().any(|entry| {
                let entry = entry.unwrap();
                let name = entry.file_name().into_string().unwrap();
                name.starts_with("debug_html_test_context")
            })
        } else {
            fs::read_dir(".").unwrap().any(|entry| {
                let entry = entry.unwrap();
                let name = entry.file_name().into_string().unwrap();
                name.starts_with("debug_html_test_context")
            })
        };

        assert!(exists, "Debug HTML file was not created");

        // SAFETY: same as above.
        unsafe {
            env::remove_var("STEAM_USER_DEBUG_HTML");
        }

        if dump_dir.exists() {
            for entry in fs::read_dir(dump_dir).unwrap() {
                let entry = entry.unwrap();
                let path = entry.path();
                let name = entry.file_name().into_string().unwrap();
                if name.starts_with("debug_html_test_context") {
                    let _ = fs::remove_file(path);
                }
            }
        }
    }
}