1use crate::GraphError;
8use std::fs;
9use std::path::{Component, Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
13
14pub fn wav_bytes(data: &[f64], rate: u32) -> Result<Vec<u8>, GraphError> {
20 if let Some(index) = data.iter().position(|value| !(*value as f32).is_finite()) {
21 return Err(GraphError::NonFinite(format!("fir[{index}] as f32")));
22 }
23 let data_len = u32::try_from(data.len() * 4).expect("FIR WAV fits u32");
24 let mut out = Vec::with_capacity(44 + data.len() * 4);
25 out.extend_from_slice(b"RIFF");
26 out.extend_from_slice(&(36 + data_len).to_le_bytes());
27 out.extend_from_slice(b"WAVEfmt ");
28 out.extend_from_slice(&16_u32.to_le_bytes());
29 out.extend_from_slice(&3_u16.to_le_bytes());
30 out.extend_from_slice(&1_u16.to_le_bytes());
31 out.extend_from_slice(&rate.to_le_bytes());
32 out.extend_from_slice(&(rate * 4).to_le_bytes());
33 out.extend_from_slice(&4_u16.to_le_bytes());
34 out.extend_from_slice(&32_u16.to_le_bytes());
35 out.extend_from_slice(b"data");
36 out.extend_from_slice(&data_len.to_le_bytes());
37 for value in data {
38 out.extend_from_slice(&(*value as f32).to_le_bytes());
39 }
40 Ok(out)
41}
42
43pub fn safe_name(name: &str) -> String {
45 let safe: String = name
46 .chars()
47 .map(|ch| {
48 if ch.is_ascii() && (ch.is_ascii_alphanumeric() || "-_.".contains(ch)) {
49 ch
50 } else {
51 '_'
52 }
53 })
54 .collect();
55 if safe.is_empty() {
56 "secs_calibration".into()
57 } else {
58 safe
59 }
60}
61
62fn safe_bundle_path(path: &Path) -> bool {
63 !path.as_os_str().is_empty()
64 && path
65 .components()
66 .all(|component| matches!(component, Component::Normal(_)))
67}
68
69#[doc(hidden)]
71pub fn write_bundle_observed(
72 root: &Path,
73 files: &[(PathBuf, Vec<u8>)],
74 entry: &Path,
75 after_stage_write: &dyn Fn(usize, &Path) -> std::io::Result<()>,
76) -> Result<(), std::io::Error> {
77 if !safe_bundle_path(entry)
78 || !files.iter().any(|(path, _)| path == entry)
79 || files.iter().any(|(path, _)| !safe_bundle_path(path))
80 {
81 return Err(std::io::Error::new(
82 std::io::ErrorKind::InvalidInput,
83 "bundle paths must be relative file paths and include the entry",
84 ));
85 }
86 let parent = root.parent().unwrap_or_else(|| Path::new("."));
87 fs::create_dir_all(parent)?;
88 let folder = root
89 .file_name()
90 .and_then(|name| name.to_str())
91 .unwrap_or("export");
92 let mut staging = None;
93 for _ in 0..100 {
94 let serial = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
95 let candidate = parent.join(format!(".{folder}.stage-{}-{serial}", std::process::id()));
96 match fs::create_dir(&candidate) {
97 Ok(()) => {
98 staging = Some(candidate);
99 break;
100 }
101 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
102 Err(error) => return Err(error),
103 }
104 }
105 let Some(staging) = staging else {
106 return Err(std::io::Error::new(
107 std::io::ErrorKind::AlreadyExists,
108 "could not create export staging directory",
109 ));
110 };
111 let stage_result = (|| {
112 use std::io::Write as _;
113 for (index, (relative, bytes)) in files.iter().enumerate() {
114 let path = staging.join(relative);
115 if let Some(parent) = path.parent() {
116 fs::create_dir_all(parent)?;
117 }
118 let mut file = fs::OpenOptions::new()
119 .write(true)
120 .create_new(true)
121 .open(&path)?;
122 file.write_all(bytes)?;
123 file.sync_all()?;
124 after_stage_write(index, &path)?;
125 }
126 Ok::<(), std::io::Error>(())
127 })();
128 if let Err(error) = stage_result {
129 let _ = fs::remove_dir_all(&staging);
130 return Err(error);
131 }
132
133 fs::create_dir_all(root)?;
134 let install = |relative: &Path| -> Result<(), std::io::Error> {
135 let destination = root.join(relative);
136 if let Some(parent) = destination.parent() {
137 fs::create_dir_all(parent)?;
138 }
139 fs::rename(staging.join(relative), destination)
140 };
141 let result = (|| {
142 for (relative, _) in files.iter().filter(|(path, _)| path != entry) {
143 install(relative)?;
144 }
145 install(entry)
146 })();
147 let _ = fs::remove_dir_all(&staging);
148 result
149}