Skip to content

Commit 411105b

Browse files
authored
Merge pull request #60 from OpenMined/madhava/wasm-fixes
Madhava/wasm fixes
2 parents 2509e30 + bd8a5c6 commit 411105b

20 files changed

Lines changed: 627 additions & 168 deletions

rust/bioscript-formats/src/genotype.rs

Lines changed: 33 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{
22
collections::HashMap,
33
fs::File,
4-
io::{BufReader, Cursor},
4+
io::{BufRead, BufReader, Cursor},
55
path::Path,
66
};
77

@@ -16,6 +16,7 @@ mod common;
1616
mod cram_backend;
1717
mod delimited;
1818
mod io;
19+
mod loaders;
1920
mod types;
2021
mod vcf;
2122
mod vcf_tokens;
@@ -32,7 +33,7 @@ use cram_backend::{
3233
};
3334
pub use cram_backend::{observe_cram_indel_with_reader, observe_cram_snp_with_reader};
3435
pub(crate) use delimited::{
35-
DelimitedColumnIndexes, Delimiter, detect_delimiter, parse_streaming_row,
36+
COMMENT_PREFIXES, DelimitedColumnIndexes, Delimiter, detect_delimiter, parse_streaming_row,
3637
};
3738
#[cfg(test)]
3839
use delimited::{GENOTYPE_ALIASES, split_csv_line, strip_bom, strip_inline_comment};
@@ -44,31 +45,27 @@ use delimited::{
4445
};
4546
#[cfg(test)]
4647
use io::looks_like_vcf_lines;
47-
use io::{
48-
detect_source_format, is_bgzf_path, read_lines_from_reader, read_zip_entry_limited,
49-
select_zip_entry,
50-
};
48+
use io::{detect_source_format, is_bgzf_path, read_lines_from_reader, select_zip_entry};
5149
pub use types::{
5250
BackendCapabilities, GenotypeLoadOptions, GenotypeSourceFormat, GenotypeStore, QueryKind,
5351
};
5452
use types::{CramBackend, DelimitedBackend, QueryBackend, RsidMapBackend, VcfBackend};
53+
pub use vcf::{
54+
choose_variant_locus_for_assembly, imputed_reference_observation, observe_vcf_snp_with_reader,
55+
observe_vcf_variant_with_reader,
56+
};
5557
#[cfg(test)]
5658
use vcf::{
57-
choose_variant_locus_for_assembly, detect_vcf_assembly, extract_vcf_sample_genotype,
58-
normalize_chromosome_name, parse_vcf_record, vcf_row_matches_variant,
59-
};
60-
pub use vcf::{
61-
imputed_reference_observation, observe_vcf_snp_with_reader, observe_vcf_variant_with_reader,
59+
detect_vcf_assembly, extract_vcf_sample_genotype, normalize_chromosome_name, parse_vcf_record,
60+
vcf_row_matches_variant,
6261
};
6362
use vcf::{lookup_indexed_vcf_variants, scan_vcf_variants};
64-
use vcf_tokens::genotype_from_vcf_gt;
63+
pub(crate) use vcf_tokens::genotype_from_vcf_gt;
6564
#[cfg(test)]
6665
use vcf_tokens::{
6766
is_symbolic_vcf_alt, normalize_sequence_token, vcf_alt_token, vcf_reference_token,
6867
};
6968

70-
const MAX_ZIP_ENTRY_BYTES: u64 = 128 * 1024 * 1024;
71-
7269
impl GenotypeStore {
7370
pub fn from_file(path: &Path) -> Result<Self, RuntimeError> {
7471
Self::from_file_with_options(path, &GenotypeLoadOptions::default())
@@ -102,6 +99,7 @@ impl GenotypeStore {
10299
backend: QueryBackend::RsidMap(RsidMapBackend {
103100
format: GenotypeSourceFormat::Text,
104101
values: HashMap::new(),
102+
source_lines: HashMap::new(),
105103
}),
106104
}
107105
}
@@ -131,13 +129,11 @@ impl GenotypeStore {
131129
if lower.ends_with(".zip") {
132130
return Self::from_zip_bytes(name, bytes);
133131
}
132+
let reader = BufReader::new(Cursor::new(bytes));
134133
if lower.ends_with(".vcf") {
135-
let lines =
136-
read_lines_from_reader(BufReader::new(Cursor::new(bytes)), Path::new(name))?;
137-
return Self::from_vcf_lines(lines);
134+
return Self::from_vcf_reader(reader, name);
138135
}
139-
let lines = read_lines_from_reader(BufReader::new(Cursor::new(bytes)), Path::new(name))?;
140-
Self::from_delimited_lines(GenotypeSourceFormat::Text, lines)
136+
Self::from_delimited_reader(GenotypeSourceFormat::Text, reader, name)
141137
}
142138

143139
fn from_zip_bytes(name: &str, bytes: &[u8]) -> Result<Self, RuntimeError> {
@@ -168,22 +164,22 @@ impl GenotypeStore {
168164
"zip archive {name} does not contain a supported genotype file"
169165
))
170166
})?;
171-
let mut entry = archive.by_name(&selected).map_err(|err| {
167+
let entry = archive.by_name(&selected).map_err(|err| {
172168
RuntimeError::Io(format!(
173169
"failed to open genotype entry {selected} in {name}: {err}"
174170
))
175171
})?;
176-
let contents = read_zip_entry_limited(
177-
&mut entry,
178-
MAX_ZIP_ENTRY_BYTES,
179-
&format!("genotype entry {selected} in {name}"),
180-
)?;
181-
let lines =
182-
read_lines_from_reader(BufReader::new(Cursor::new(contents)), Path::new(&selected))?;
172+
let label = format!("genotype entry {selected} in {name}");
173+
// Stream-decompress directly off the zip reader so we never have to
174+
// materialize the entire decompressed entry in memory. GenesForGood
175+
// exports decompress to >128MB which used to trip the old
176+
// `read_zip_entry_limited` cap; the cap is gone because the streaming
177+
// parser keeps memory bounded to the rsid map itself.
178+
let reader = BufReader::new(entry);
183179
if selected.to_ascii_lowercase().ends_with(".vcf") {
184-
return Self::from_vcf_lines(lines);
180+
return Self::from_vcf_reader(reader, &label);
185181
}
186-
Self::from_delimited_lines(GenotypeSourceFormat::Zip, lines)
182+
Self::from_delimited_reader(GenotypeSourceFormat::Zip, reader, &label)
187183
}
188184

189185
fn from_vcf_file(path: &Path, options: &GenotypeLoadOptions) -> Self {
@@ -236,63 +232,20 @@ impl GenotypeStore {
236232
})
237233
}
238234

239-
fn from_vcf_lines(lines: Vec<String>) -> Result<Self, RuntimeError> {
240-
let mut values = HashMap::new();
241-
242-
for line in lines {
243-
let trimmed = line.trim();
244-
if trimmed.is_empty() || trimmed.starts_with("##") || trimmed.starts_with("#CHROM") {
245-
continue;
246-
}
247-
248-
let fields: Vec<&str> = trimmed.split('\t').collect();
249-
if fields.len() < 10 {
250-
continue;
251-
}
252-
253-
let rsid = fields[2].trim();
254-
if rsid.is_empty() || rsid == "." {
255-
continue;
256-
}
257-
258-
let reference = fields[3].trim();
259-
let alternates: Vec<&str> = fields[4]
260-
.split(',')
261-
.map(str::trim)
262-
.filter(|alt| !alt.is_empty() && *alt != ".")
263-
.collect();
264-
if reference.is_empty() || alternates.is_empty() {
265-
continue;
266-
}
267-
268-
let sample_gt = fields[9].split(':').next().unwrap_or(".");
269-
if let Some(genotype) = genotype_from_vcf_gt(sample_gt, reference, &alternates) {
270-
values.insert(rsid.to_owned(), genotype);
271-
}
272-
}
273-
274-
Ok(Self::from_rsid_map(GenotypeSourceFormat::Vcf, values))
235+
fn from_vcf_reader<R: BufRead>(reader: R, label: &str) -> Result<Self, RuntimeError> {
236+
loaders::from_vcf_reader(reader, label)
275237
}
276238

277-
fn from_delimited_lines(
239+
fn from_delimited_reader<R: BufRead>(
278240
format: GenotypeSourceFormat,
279-
lines: Vec<String>,
241+
reader: R,
242+
label: &str,
280243
) -> Result<Self, RuntimeError> {
281-
let delimiter = detect_delimiter(&lines);
282-
let mut parser = RowParser::new(delimiter);
283-
let mut values = HashMap::new();
284-
for line in lines {
285-
if let Some((rsid, genotype)) = parser.consume_line(&line)? {
286-
values.insert(rsid, genotype);
287-
}
288-
}
289-
Ok(Self::from_rsid_map(format, values))
244+
loaders::from_delimited_reader(format, reader, label)
290245
}
291246

292-
fn from_rsid_map(format: GenotypeSourceFormat, values: HashMap<String, String>) -> Self {
293-
Self {
294-
backend: QueryBackend::RsidMap(RsidMapBackend { format, values }),
295-
}
247+
fn from_vcf_lines(lines: Vec<String>) -> Result<Self, RuntimeError> {
248+
loaders::from_vcf_lines(lines)
296249
}
297250

298251
fn from_delimited_file(
@@ -1683,15 +1636,4 @@ mod tests {
16831636
.unwrap_err();
16841637
assert!(err.to_string().contains("invalid VCF position"));
16851638
}
1686-
1687-
#[test]
1688-
fn zip_entry_limited_reader_rejects_oversized_output() {
1689-
let mut reader = std::io::Cursor::new(b"abcdef".to_vec());
1690-
let err = read_zip_entry_limited(&mut reader, 5, "test zip entry").unwrap_err();
1691-
assert!(
1692-
err.to_string()
1693-
.contains("test zip entry exceeds decompressed limit of 5 bytes"),
1694-
"{err}"
1695-
);
1696-
}
16971639
}

rust/bioscript-formats/src/genotype/backends.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ impl RsidMapBackend {
2222
) -> Result<VariantObservation, RuntimeError> {
2323
for rsid in &variant.rsids {
2424
if let Some(value) = self.values.get(rsid) {
25+
let mut evidence = vec![format!("resolved by rsid {rsid}")];
26+
// Mirror DelimitedBackend's `| source line: …` evidence so
27+
// wasm-side from_bytes loads produce byte-identical reports
28+
// to the CLI's path-backed DelimitedBackend.
29+
if let Some(source) = self.source_lines.get(rsid) {
30+
evidence.push(format!("source line: {source}"));
31+
}
2532
return Ok(VariantObservation {
2633
backend: self.backend_name().to_owned(),
2734
matched_rsid: Some(rsid.clone()),
2835
genotype: Some(value.clone()),
29-
evidence: vec![format!("resolved by rsid {rsid}")],
36+
evidence,
3037
..VariantObservation::default()
3138
});
3239
}

rust/bioscript-formats/src/genotype/delimited.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod scan;
88

99
pub(crate) use scan::scan_delimited_variants;
1010

11-
const COMMENT_PREFIXES: [&str; 2] = ["#", "//"];
11+
pub(crate) const COMMENT_PREFIXES: [&str; 2] = ["#", "//"];
1212
const RSID_ALIASES: &[&str] = &["rsid", "name", "snp", "marker", "id", "snpid"];
1313
const CHROM_ALIASES: &[&str] = &["chromosome", "chr", "chrom"];
1414
const POSITION_ALIASES: &[&str] = &[

rust/bioscript-formats/src/genotype/io.rs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{
22
fs::File,
3-
io::{BufRead, BufReader, Read},
3+
io::{BufRead, BufReader},
44
path::Path,
55
};
66

@@ -96,24 +96,6 @@ pub(crate) fn read_lines_from_reader<R: BufRead>(
9696
Ok(lines)
9797
}
9898

99-
pub(crate) fn read_zip_entry_limited<R: Read>(
100-
reader: &mut R,
101-
max_bytes: u64,
102-
label: &str,
103-
) -> Result<Vec<u8>, RuntimeError> {
104-
let mut contents = Vec::new();
105-
reader
106-
.take(max_bytes.saturating_add(1))
107-
.read_to_end(&mut contents)
108-
.map_err(|err| RuntimeError::Io(format!("failed to read {label}: {err}")))?;
109-
if u64::try_from(contents.len()).unwrap_or(u64::MAX) > max_bytes {
110-
return Err(RuntimeError::InvalidArguments(format!(
111-
"{label} exceeds decompressed limit of {max_bytes} bytes"
112-
)));
113-
}
114-
Ok(contents)
115-
}
116-
11799
pub(crate) fn detect_source_format(
118100
path: &Path,
119101
forced: Option<GenotypeSourceFormat>,

0 commit comments

Comments
 (0)