11use 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;
1616mod cram_backend;
1717mod delimited;
1818mod io;
19+ mod loaders;
1920mod types;
2021mod vcf;
2122mod vcf_tokens;
@@ -32,7 +33,7 @@ use cram_backend::{
3233} ;
3334pub use cram_backend:: { observe_cram_indel_with_reader, observe_cram_snp_with_reader} ;
3435pub ( 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) ]
3839use delimited:: { GENOTYPE_ALIASES , split_csv_line, strip_bom, strip_inline_comment} ;
@@ -44,31 +45,27 @@ use delimited::{
4445} ;
4546#[ cfg( test) ]
4647use 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} ;
5149pub use types:: {
5250 BackendCapabilities , GenotypeLoadOptions , GenotypeSourceFormat , GenotypeStore , QueryKind ,
5351} ;
5452use 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) ]
5658use 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} ;
6362use 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) ]
6665use 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-
7269impl 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}
0 commit comments