openstranded_s2mod_tool/
util.rs1use std::fs;
20use std::path::{Path, PathBuf};
21
22use anyhow::{Context, Result};
23use serde::Serialize;
24
25pub fn relative_path(file: &Path, input_root: &Path) -> PathBuf {
27 file.strip_prefix(input_root).unwrap_or(file).to_path_buf()
28}
29
30pub fn read_file_lossy(path: &Path) -> Result<String> {
34 let bytes = fs::read(path).with_context(|| format!("reading {:?}", path))?;
35 if let Ok(s) = String::from_utf8(bytes.clone()) {
36 return Ok(s);
37 }
38 Ok(bytes.iter().map(|&b| b as char).collect())
39}
40
41pub fn parse_inf_file(path: &Path) -> Result<Vec<inf2ron::InfEntry>> {
43 let content = read_file_lossy(path)?;
44 inf2ron::InfParser::parse_str(&content).map_err(|e| anyhow::anyhow!("{}", e))
45}
46
47pub fn write_ron<T: Serialize + ?Sized>(path: &Path, data: &T) -> Result<()> {
49 let ron_str = ron::to_string(data)?;
50 if let Some(parent) = path.parent() {
51 fs::create_dir_all(parent)?;
52 }
53 fs::write(path, &ron_str)?;
54 Ok(())
55}
56
57pub fn copy_to_staging(file: &Path, input_root: &Path, stage: &Path) -> Result<()> {
60 let relative = relative_path(file, input_root);
61 let dest = stage.join(relative);
62 if let Some(parent) = dest.parent() {
63 fs::create_dir_all(parent)?;
64 }
65 fs::copy(file, &dest)?;
66 Ok(())
67}
68
69pub fn normalize_id(name: &str) -> String {
72 let mut out = String::with_capacity(name.len());
73 let mut prev_was_space = false;
74 for ch in name.chars() {
75 if ch.is_ascii_alphanumeric() {
76 out.push(ch.to_ascii_lowercase());
77 prev_was_space = false;
78 } else if ch == '_' || ch == '-' {
79 out.push(ch);
80 prev_was_space = false;
81 } else if ch.is_ascii_whitespace() {
82 if !prev_was_space {
83 out.push('_');
84 prev_was_space = true;
85 }
86 } else {
87 if !prev_was_space && !out.is_empty() {
88 out.push('_');
89 prev_was_space = true;
90 }
91 }
92 }
93 let trimmed = out.trim_end_matches('_').to_string();
94 if trimmed.is_empty() { "content".to_string() } else { trimmed }
95}