1use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::error::{Error, Result};
11use crate::table::{TableAlign, TableData, convert_table_pages};
12
13const MAX_WIG_BYTES: u64 = 128 * 1024 * 1024;
14const MAX_WIG_LINES: usize = 2_000_000;
15const MAX_WIG_LINE_BYTES: usize = 1 << 20;
16const MAX_WIG_VALUES: usize = 100_000;
17const MAX_WIG_VALUE_BYTES: usize = 64 * 1024;
18
19pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
20 let Ok(text) = std::str::from_utf8(prefix) else {
21 return false;
22 };
23 text.lines().map(str::trim).any(|line| {
24 let lower = line.to_ascii_lowercase();
25 lower.starts_with("fixedstep ")
26 || lower.starts_with("variablestep ")
27 || lower.contains("type=wiggle_0")
28 })
29}
30
31pub(crate) fn convert(
32 path: &Path,
33 options: &ConvertOptions,
34 sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36 let bytes = read_limited_file(
37 path,
38 options.max_input_bytes.min(MAX_WIG_BYTES),
39 "WIG input",
40 )?;
41 let text = String::from_utf8(bytes)
42 .map_err(|error| Error::InvalidInput(format!("WIG input must be UTF-8/ASCII: {error}")))?;
43 let (mut table, warnings) = parse_wig(&text)?;
44 let mut page_sink = WigPageSink {
45 inner: sink,
46 warnings: &warnings,
47 };
48 convert_table_pages(&mut table, "wig", options, &mut page_sink)?;
49 Ok(warnings)
50}
51
52struct WigPageSink<'a> {
53 inner: &'a mut dyn PageConsumer,
54 warnings: &'a [String],
55}
56impl PageConsumer for WigPageSink<'_> {
57 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
58 page.source_format = "wig".into();
59 page.title = "WIG continuous signal".into();
60 page.description =
61 "UCSC fixedStep/variableStep values are displayed as inert one-based closed intervals"
62 .into();
63 for warning in self.warnings {
64 page.warn(warning.clone());
65 }
66 self.inner.consume(page)
67 }
68}
69
70#[derive(Clone, Debug)]
71enum Mode {
72 Fixed {
73 chrom: String,
74 next_start: u64,
75 step: u64,
76 span: u64,
77 },
78 Variable {
79 chrom: String,
80 span: u64,
81 previous_start: Option<u64>,
82 },
83}
84
85fn parse_wig(text: &str) -> Result<(TableData, Vec<String>)> {
86 if text.len() as u64 > MAX_WIG_BYTES {
87 return Err(Error::LimitExceeded(format!(
88 "WIG input exceeds {MAX_WIG_BYTES} bytes"
89 )));
90 }
91 let lines = text.lines().collect::<Vec<_>>();
92 if lines.len() > MAX_WIG_LINES {
93 return Err(Error::LimitExceeded(format!(
94 "WIG input exceeds {MAX_WIG_LINES} lines"
95 )));
96 }
97 let mut rows = Vec::new();
98 let mut warnings = Vec::new();
99 let mut mode = None::<Mode>;
100 let mut directives = false;
101 for (line_number, original) in lines.iter().enumerate() {
102 if original.len() > MAX_WIG_LINE_BYTES {
103 return Err(Error::LimitExceeded(format!(
104 "WIG line {} exceeds {MAX_WIG_LINE_BYTES} bytes",
105 line_number + 1
106 )));
107 }
108 let line = original.trim();
109 if line.is_empty() || line.starts_with('#') {
110 continue;
111 }
112 let lower = line.to_ascii_lowercase();
113 if lower.starts_with("track") || lower.starts_with("browser") {
114 directives = true;
115 continue;
116 }
117 if lower.starts_with("fixedstep") {
118 let fields = parse_declaration(line, "fixedStep")?;
119 let chrom = required_field(&fields, "chrom")?;
120 let start = parse_positive(&fields, "start")?;
121 let step = parse_positive_or_default(&fields, "step", 1)?;
122 let span = parse_positive_or_default(&fields, "span", 1)?;
123 mode = Some(Mode::Fixed {
124 chrom,
125 next_start: start,
126 step,
127 span,
128 });
129 continue;
130 }
131 if lower.starts_with("variablestep") {
132 let fields = parse_declaration(line, "variableStep")?;
133 let chrom = required_field(&fields, "chrom")?;
134 let span = parse_positive_or_default(&fields, "span", 1)?;
135 mode = Some(Mode::Variable {
136 chrom,
137 span,
138 previous_start: None,
139 });
140 continue;
141 }
142 let active = mode.as_mut().ok_or_else(|| {
143 Error::InvalidInput(format!(
144 "WIG data line {} appears before fixedStep/variableStep declaration",
145 line_number + 1
146 ))
147 })?;
148 match active {
149 Mode::Fixed {
150 chrom,
151 next_start,
152 step,
153 span,
154 } => {
155 for token in line.split_ascii_whitespace() {
156 let value = parse_signal(token, line_number + 1)?;
157 let start = *next_start;
158 let end = start.checked_add(*span - 1).ok_or_else(|| {
159 Error::LimitExceeded("WIG fixedStep coordinate overflowed".into())
160 })?;
161 rows.push(vec![
162 chrom.clone(),
163 start.to_string(),
164 end.to_string(),
165 value.to_string(),
166 ]);
167 *next_start = start.checked_add(*step).ok_or_else(|| {
168 Error::LimitExceeded("WIG fixedStep step overflowed".into())
169 })?;
170 if rows.len() > MAX_WIG_VALUES {
171 return Err(Error::LimitExceeded(format!(
172 "WIG exceeds {MAX_WIG_VALUES} values"
173 )));
174 }
175 }
176 }
177 Mode::Variable {
178 chrom,
179 span,
180 previous_start,
181 } => {
182 let tokens = line.split_ascii_whitespace().collect::<Vec<_>>();
183 if tokens.len() != 2 {
184 return Err(Error::InvalidInput(format!(
185 "WIG variableStep line {} must contain position and value",
186 line_number + 1
187 )));
188 }
189 let start = tokens[0].parse::<u64>().map_err(|_| {
190 Error::InvalidInput(format!("WIG line {} position is invalid", line_number + 1))
191 })?;
192 if start == 0 {
193 return Err(Error::InvalidInput(format!(
194 "WIG line {} position must be one-based",
195 line_number + 1
196 )));
197 }
198 if previous_start.is_some_and(|previous| start <= previous) {
199 return Err(Error::InvalidInput(format!(
200 "WIG line {} variableStep positions are not increasing",
201 line_number + 1
202 )));
203 }
204 let end = start.checked_add(*span - 1).ok_or_else(|| {
205 Error::LimitExceeded("WIG variableStep coordinate overflowed".into())
206 })?;
207 let value = parse_signal(tokens[1], line_number + 1)?;
208 rows.push(vec![
209 chrom.clone(),
210 start.to_string(),
211 end.to_string(),
212 value.to_string(),
213 ]);
214 *previous_start = Some(start);
215 if rows.len() > MAX_WIG_VALUES {
216 return Err(Error::LimitExceeded(format!(
217 "WIG exceeds {MAX_WIG_VALUES} values"
218 )));
219 }
220 }
221 }
222 }
223 if rows.is_empty() {
224 return Err(Error::InvalidInput("WIG contains no signal values".into()));
225 }
226 if directives {
227 warnings.push("WIG track/browser directives were ignored".into());
228 }
229 let headers = ["chrom", "chromStart", "chromEnd", "dataValue"]
230 .into_iter()
231 .map(str::to_owned)
232 .collect();
233 let alignments = vec![
234 TableAlign::Left,
235 TableAlign::Right,
236 TableAlign::Right,
237 TableAlign::Right,
238 ];
239 Ok((
240 TableData {
241 headers,
242 rows,
243 alignments,
244 raw_source: String::new(),
245 },
246 warnings,
247 ))
248}
249
250fn parse_declaration(
251 line: &str,
252 keyword: &str,
253) -> Result<std::collections::HashMap<String, String>> {
254 let mut parts = line.split_ascii_whitespace();
255 let found = parts.next().unwrap_or_default();
256 if !found.eq_ignore_ascii_case(keyword) {
257 return Err(Error::InvalidInput(format!(
258 "WIG declaration {found:?} is invalid"
259 )));
260 }
261 let mut fields = std::collections::HashMap::new();
262 for token in parts {
263 let Some((key, value)) = token.split_once('=') else {
264 return Err(Error::InvalidInput(format!(
265 "WIG declaration token {token:?} is invalid"
266 )));
267 };
268 if key.is_empty() || value.is_empty() {
269 return Err(Error::InvalidInput(
270 "WIG declaration has an empty key/value".into(),
271 ));
272 }
273 fields.insert(key.to_ascii_lowercase(), value.trim_matches('"').to_owned());
274 }
275 Ok(fields)
276}
277
278fn required_field(fields: &std::collections::HashMap<String, String>, key: &str) -> Result<String> {
279 fields
280 .get(key)
281 .filter(|value| !value.is_empty())
282 .cloned()
283 .ok_or_else(|| Error::InvalidInput(format!("WIG declaration is missing {key}")))
284}
285fn parse_positive(fields: &std::collections::HashMap<String, String>, key: &str) -> Result<u64> {
286 let value = fields
287 .get(key)
288 .ok_or_else(|| Error::InvalidInput(format!("WIG declaration is missing {key}")))?;
289 let value = value
290 .parse::<u64>()
291 .map_err(|_| Error::InvalidInput(format!("WIG {key} is invalid")))?;
292 if value == 0 {
293 return Err(Error::InvalidInput(format!("WIG {key} must be positive")));
294 }
295 Ok(value)
296}
297fn parse_positive_or_default(
298 fields: &std::collections::HashMap<String, String>,
299 key: &str,
300 default: u64,
301) -> Result<u64> {
302 match fields.get(key) {
303 None => Ok(default),
304 Some(value) => {
305 let value = value
306 .parse::<u64>()
307 .map_err(|_| Error::InvalidInput(format!("WIG {key} is invalid")))?;
308 if value == 0 {
309 return Err(Error::InvalidInput(format!("WIG {key} must be positive")));
310 }
311 Ok(value)
312 }
313 }
314}
315fn parse_signal(value: &str, line: usize) -> Result<f64> {
316 if value.len() > MAX_WIG_VALUE_BYTES {
317 return Err(Error::LimitExceeded(format!(
318 "WIG line {line} value exceeds {MAX_WIG_VALUE_BYTES} bytes"
319 )));
320 }
321 let value = value
322 .parse::<f64>()
323 .map_err(|_| Error::InvalidInput(format!("WIG line {line} signal value is invalid")))?;
324 if !value.is_finite() {
325 return Err(Error::InvalidInput(format!(
326 "WIG line {line} signal is non-finite"
327 )));
328 }
329 Ok(value)
330}