htslib 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44b02c34b18d412043b9063ef1d8da5f9f9f88bd4bdfcf7c8537e0a1dbcebe14
4
- data.tar.gz: cc76ea23d6fa215755a9c0ccaab3de11e5e2465b877e3481565b9586d9a730d8
3
+ metadata.gz: eb6716391f6e8aa27cb4859140deb80073a7b02e149f17704a4b08e125567cb9
4
+ data.tar.gz: 20ec9fc316dd378bd869d590360fc8d50a9b2be075871e76a1b4148292614cf6
5
5
  SHA512:
6
- metadata.gz: 726b1885741d8091cac5810a1522523cdc2dda959f244fe00a34a02a12ef73b6cfa5b1f5982538a4ca18bfb0b94e5d9e3ec30699ab2f4cd33f6206e68d01061b
7
- data.tar.gz: 47055c773ce723e100e775528d77db8e6755f2f0b167f0d20037c461009453f3979260f010bb54a9e177a1a51ffa624dea7bda3d9ad04f8efd441b5d13d58c07
6
+ metadata.gz: ed55104f781970f32e5918d15fbe2d0cf5c08e01524c29cf95656b6da902a1528f7630152ae8eb9adee815442230e74868707d348bc7947a871b9576d44bcf0d
7
+ data.tar.gz: c21d82a89b123e25e135486844d09ba83734fb1149269a158b1f2b81be94708ba3e55f95ee94d3a54e78b40a40db7ebcc71fdce969ecc16950802f84bee79461
data/README.md CHANGED
@@ -122,12 +122,12 @@ bcf = HTS::Bcf.open("test/fixtures/test.bcf")
122
122
 
123
123
  bcf.each do |r|
124
124
  p chrom: r.chrom,
125
- pos: r.pos,
125
+ pos: r.pos + 1, # VCF POS is 1-based; Record#pos is 0-based.
126
126
  id: r.id,
127
127
  qual: r.qual.round(2),
128
128
  ref: r.ref,
129
129
  alt: r.alt,
130
- filter: r.filter,
130
+ filters: r.filter,
131
131
  info: r.info.to_h,
132
132
  format: r.format.to_h
133
133
  end
@@ -157,6 +157,7 @@ fa.close
157
157
 
158
158
  ```ruby
159
159
  tb = HTS::Tabix.open("test/fixtures/test.vcf.gz")
160
+ # Numeric coordinates are 0-based, half-open: [2000, 3000).
160
161
  tb.query("poo", 2000, 3000) do |line|
161
162
  puts line.join("\t")
162
163
  end
@@ -191,7 +192,8 @@ silently returning overwritten data. `genotype_strings`, `get`, and `[]` remain
191
192
  the allocating convenience APIs. Use `unpack: :site_only` when FORMAT columns
192
193
  are not needed.
193
194
 
194
- Tabix likewise separates raw and materialized access:
195
+ Tabix likewise lets callers avoid splitting unused columns. All yielded Ruby
196
+ strings and arrays below are owning values:
195
197
 
196
198
  ```ruby
197
199
  tb.each_line("chr1:1-1000") { |line| }
data/TUTORIAL.md CHANGED
@@ -331,12 +331,12 @@ bcf = HTS::Bcf.open("b.bcf")
331
331
 
332
332
  bcf.each do |r|
333
333
  p chrom: r.chrom,
334
- pos: r.pos,
334
+ pos: r.pos + 1, # VCF POS is 1-based; Record#pos is 0-based.
335
335
  id: r.id,
336
336
  qual: r.qual.round(2),
337
337
  ref: r.ref,
338
338
  alt: r.alt,
339
- filter: r.filter,
339
+ filters: r.filter,
340
340
  info: r.info.to_h,
341
341
  format: r.format.to_h
342
342
  end
@@ -242,21 +242,25 @@ static VALUE native_record_replace(VALUE self, VALUE qname, VALUE flag, VALUE ti
242
242
  bam1_t *record = get_record(self)->pointer;
243
243
  long cigar_count, i;
244
244
  uint32_t *cigar = NULL;
245
+ VALUE cigar_storage = 0;
245
246
  const char *quality_data = NULL;
246
247
  int result;
247
248
 
248
249
  StringValue(qname);
249
250
  StringValue(sequence);
251
+ if (memchr(RSTRING_PTR(qname), '\0', RSTRING_LEN(qname))) {
252
+ rb_raise(rb_eArgError, "qname must not contain NUL bytes");
253
+ }
250
254
  Check_Type(cigar_value, T_ARRAY);
251
255
  cigar_count = RARRAY_LEN(cigar_value);
252
256
  if (cigar_count > 0) {
253
- cigar = ALLOC_N(uint32_t, cigar_count);
257
+ cigar = ALLOCV_N(uint32_t, cigar_storage, cigar_count);
254
258
  for (i = 0; i < cigar_count; i++) cigar[i] = NUM2UINT(rb_ary_entry(cigar_value, i));
255
259
  }
256
260
  if (!NIL_P(qualities)) {
257
261
  StringValue(qualities);
258
262
  if (RSTRING_LEN(qualities) != RSTRING_LEN(sequence)) {
259
- if (cigar) xfree(cigar);
263
+ ALLOCV_END(cigar_storage);
260
264
  rb_raise(rb_eArgError, "qualities length must match sequence length");
261
265
  }
262
266
  quality_data = RSTRING_PTR(qualities);
@@ -264,12 +268,12 @@ static VALUE native_record_replace(VALUE self, VALUE qname, VALUE flag, VALUE ti
264
268
 
265
269
  errno = 0;
266
270
  result = bam_set1(record,
267
- (size_t)RSTRING_LEN(qname) + 1, RSTRING_PTR(qname),
271
+ (size_t)RSTRING_LEN(qname), RSTRING_PTR(qname),
268
272
  NUM2UINT(flag), NUM2INT(tid), NUM2LL(pos), NUM2UINT(mapq),
269
273
  (size_t)cigar_count, cigar,
270
274
  NUM2INT(mtid), NUM2LL(mpos), NUM2LL(isize),
271
275
  (size_t)RSTRING_LEN(sequence), RSTRING_PTR(sequence), quality_data, 0);
272
- if (cigar) xfree(cigar);
276
+ ALLOCV_END(cigar_storage);
273
277
  if (result < 0) {
274
278
  if (errno) rb_sys_fail("bam_set1");
275
279
  rb_raise(rb_eRuntimeError, "bam_set1 failed: %d", result);
@@ -282,6 +286,9 @@ static VALUE native_record_set_qname(VALUE self, VALUE name) {
282
286
  if (result < 0) rb_raise(rb_eRuntimeError, "bam_set_qname failed: %d", result);
283
287
  return name;
284
288
  }
289
+ static void native_record_update_bin(bam1_t *record) {
290
+ record->core.bin = hts_reg2bin(record->core.pos, bam_endpos(record), 14, 5);
291
+ }
285
292
  static VALUE native_record_core_get(VALUE self, VALUE field) {
286
293
  bam1_core_t *core = &get_record(self)->pointer->core;
287
294
  ID id = SYM2ID(field);
@@ -297,7 +304,8 @@ static VALUE native_record_core_get(VALUE self, VALUE field) {
297
304
  rb_raise(rb_eArgError, "unknown BAM core field");
298
305
  }
299
306
  static VALUE native_record_core_set(VALUE self, VALUE field, VALUE new_value) {
300
- bam1_core_t *core = &get_record(self)->pointer->core;
307
+ bam1_t *record = get_record(self)->pointer;
308
+ bam1_core_t *core = &record->core;
301
309
  ID id = SYM2ID(field);
302
310
  if (id == rb_intern("tid")) core->tid = NUM2INT(new_value);
303
311
  else if (id == rb_intern("mtid")) core->mtid = NUM2INT(new_value);
@@ -308,6 +316,7 @@ static VALUE native_record_core_set(VALUE self, VALUE field, VALUE new_value) {
308
316
  else if (id == rb_intern("mapq")) core->qual = NUM2UINT(new_value);
309
317
  else if (id == rb_intern("flag")) core->flag = NUM2UINT(new_value);
310
318
  else rb_raise(rb_eArgError, "unknown or read-only BAM core field");
319
+ if (id == rb_intern("pos") || id == rb_intern("flag")) native_record_update_bin(record);
311
320
  return new_value;
312
321
  }
313
322
  static VALUE native_record_endpos(VALUE self) { return LL2NUM(bam_endpos(get_record(self)->pointer)); }
@@ -320,8 +329,10 @@ static VALUE native_record_cigar_values(VALUE self) {
320
329
  return result;
321
330
  }
322
331
  static VALUE native_record_set_cigar(VALUE self, VALUE text) {
323
- int result = bam_parse_cigar(StringValueCStr(text), NULL, get_record(self)->pointer);
332
+ bam1_t *record = get_record(self)->pointer;
333
+ int result = bam_parse_cigar(StringValueCStr(text), NULL, record);
324
334
  if (result < 0) rb_raise(rb_eRuntimeError, "bam_parse_cigar failed: %d", result);
335
+ native_record_update_bin(record);
325
336
  return text;
326
337
  }
327
338
  static VALUE native_record_qlen(VALUE self) {
@@ -390,31 +401,47 @@ static VALUE native_record_to_s(VALUE self, VALUE header_value) {
390
401
  free(string.s);
391
402
  return value;
392
403
  }
404
+ typedef struct { uint32_t *cigar; long count; } cigar_result_t;
405
+ static VALUE native_cigar_result_to_array(VALUE data) {
406
+ cigar_result_t *result = (cigar_result_t *)(uintptr_t)data;
407
+ VALUE array = rb_ary_new_capa(result->count);
408
+ long i;
409
+ for (i = 0; i < result->count; i++) rb_ary_push(array, UINT2NUM(result->cigar[i]));
410
+ return array;
411
+ }
412
+ static VALUE native_cigar_result_free(VALUE data) {
413
+ cigar_result_t *result = (cigar_result_t *)(uintptr_t)data;
414
+ free(result->cigar);
415
+ result->cigar = NULL;
416
+ return Qnil;
417
+ }
393
418
  static VALUE native_cigar_parse(VALUE klass, VALUE text) {
394
419
  uint32_t *cigar = NULL;
395
420
  size_t capacity = 0;
396
- long result = sam_parse_cigar(StringValueCStr(text), NULL, &cigar, &capacity), i;
397
- VALUE array;
421
+ long result = sam_parse_cigar(StringValueCStr(text), NULL, &cigar, &capacity);
422
+ cigar_result_t parsed;
398
423
  if (result < 0) { free(cigar); rb_raise(rb_eRuntimeError, "sam_parse_cigar failed: %ld", result); }
399
- array = rb_ary_new_capa(result);
400
- for (i = 0; i < result; i++) rb_ary_push(array, UINT2NUM(cigar[i]));
401
- free(cigar);
402
- return array;
424
+ parsed.cigar = cigar;
425
+ parsed.count = result;
426
+ return rb_ensure(native_cigar_result_to_array, (VALUE)(uintptr_t)&parsed,
427
+ native_cigar_result_free, (VALUE)(uintptr_t)&parsed);
403
428
  }
404
429
  static VALUE native_cigar_qlen(VALUE klass, VALUE array) {
405
430
  long count = RARRAY_LEN(array), i;
406
- uint32_t *values = ALLOC_N(uint32_t, count ? count : 1);
431
+ VALUE storage = 0;
432
+ uint32_t *values = ALLOCV_N(uint32_t, storage, count ? count : 1);
407
433
  for (i = 0; i < count; i++) values[i] = NUM2UINT(rb_ary_entry(array, i));
408
434
  hts_pos_t result = bam_cigar2qlen(count, values);
409
- xfree(values);
435
+ ALLOCV_END(storage);
410
436
  return LL2NUM(result);
411
437
  }
412
438
  static VALUE native_cigar_rlen(VALUE klass, VALUE array) {
413
439
  long count = RARRAY_LEN(array), i;
414
- uint32_t *values = ALLOC_N(uint32_t, count ? count : 1);
440
+ VALUE storage = 0;
441
+ uint32_t *values = ALLOCV_N(uint32_t, storage, count ? count : 1);
415
442
  for (i = 0; i < count; i++) values[i] = NUM2UINT(rb_ary_entry(array, i));
416
443
  hts_pos_t result = bam_cigar2rlen(count, values);
417
- xfree(values);
444
+ ALLOCV_END(storage);
418
445
  return LL2NUM(result);
419
446
  }
420
447
 
@@ -484,9 +511,10 @@ static VALUE native_record_aux_update_array(VALUE self, VALUE key, VALUE type_va
484
511
  long count = RARRAY_LEN(array), i;
485
512
  size_t width;
486
513
  uint8_t *buffer;
514
+ VALUE buffer_storage = 0;
487
515
  int result;
488
516
  switch (type) { case 'c': case 'C': width = 1; break; case 's': case 'S': width = 2; break; default: width = 4; }
489
- buffer = ALLOC_N(uint8_t, count * width + 1);
517
+ buffer = ALLOCV_N(uint8_t, buffer_storage, count * width + 1);
490
518
  for (i = 0; i < count; i++) {
491
519
  VALUE item = rb_ary_entry(array, i);
492
520
  if (type == 'c') { int8_t v = NUM2INT(item); memcpy(buffer + i, &v, 1); }
@@ -496,10 +524,10 @@ static VALUE native_record_aux_update_array(VALUE self, VALUE key, VALUE type_va
496
524
  else if (type == 'i') { int32_t v = NUM2INT(item); memcpy(buffer + i * 4, &v, 4); }
497
525
  else if (type == 'I') { uint32_t v = NUM2UINT(item); memcpy(buffer + i * 4, &v, 4); }
498
526
  else if (type == 'f') { float v = (float)NUM2DBL(item); memcpy(buffer + i * 4, &v, 4); }
499
- else { xfree(buffer); rb_raise(rb_eArgError, "invalid AUX array type"); }
527
+ else { ALLOCV_END(buffer_storage); rb_raise(rb_eArgError, "invalid AUX array type"); }
500
528
  }
501
529
  result = bam_aux_update_array(get_record(self)->pointer, StringValueCStr(key), type, count, buffer);
502
- xfree(buffer);
530
+ ALLOCV_END(buffer_storage);
503
531
  return INT2NUM(result);
504
532
  }
505
533
  static VALUE native_record_aux_delete(VALUE self, VALUE key) {
@@ -514,6 +542,13 @@ static VALUE native_record_aux_key(VALUE self, VALUE key) {
514
542
  }
515
543
 
516
544
  /* File and iterator */
545
+ static void raise_bam_open_error(VALUE path_value, int error) {
546
+ if (error) rb_syserr_fail_str(error, path_value);
547
+ VALUE hts=rb_const_get(rb_cObject,rb_intern("HTS"));
548
+ VALUE bam=rb_const_get(hts,rb_intern("Bam"));
549
+ VALUE klass=rb_const_get(bam,rb_intern("OpenError"));
550
+ rb_raise(klass,"Failed to open %"PRIsVALUE": HTSlib could not recognize or open the input",path_value);
551
+ }
517
552
  static VALUE native_bam_open(VALUE klass, VALUE path_value, VALUE mode_value) {
518
553
  ruby_bam_file_t *value;
519
554
  VALUE object = TypedData_Make_Struct(klass, ruby_bam_file_t, &bam_file_type, value);
@@ -521,16 +556,18 @@ static VALUE native_bam_open(VALUE klass, VALUE path_value, VALUE mode_value) {
521
556
  value->active_io = 0;
522
557
  value->path = rb_str_dup(StringValue(path_value));
523
558
  RB_OBJ_WRITE(object, &value->path, value->path);
559
+ errno = 0;
524
560
  value->file = hts_open(StringValueCStr(value->path), StringValueCStr(mode_value));
525
- if (!value->file) rb_syserr_fail_str(ENOENT, path_value);
561
+ if (!value->file) raise_bam_open_error(path_value, errno);
526
562
  return object;
527
563
  }
528
564
  static VALUE native_bam_close(VALUE self) {
529
565
  ruby_bam_file_t *value = get_file(self, 1);
566
+ int result = 0;
530
567
  if (value->active_io) rb_raise(rb_eIOError, "cannot close BAM during active I/O");
531
568
  if (value->index) { hts_idx_destroy(value->index); value->index = NULL; }
532
- if (value->file) { hts_close(value->file); value->file = NULL; }
533
- return Qnil;
569
+ if (value->file) { result = hts_close(value->file); value->file = NULL; }
570
+ return INT2NUM(result);
534
571
  }
535
572
  static VALUE native_bam_closed(VALUE self) { return get_file(self, 1)->file ? Qfalse : Qtrue; }
536
573
  typedef struct { htsFile *file; sam_hdr_t *header; bam1_t *record; hts_itr_t *iterator; int result; } bam_io_args_t;
@@ -541,22 +578,29 @@ static void *bam_read_without_gvl(void *data) { bam_io_args_t *args=data; args->
541
578
  static void *bam_write_without_gvl(void *data) { bam_io_args_t *args=data; args->result=sam_write1(args->file,args->header,args->record); return NULL; }
542
579
  static void *bam_iterator_without_gvl(void *data) { bam_io_args_t *args=data; args->result=sam_itr_next(args->file,args->iterator,args->record); return NULL; }
543
580
  static void bam_io_begin(ruby_bam_file_t *file) { if(file->active_io)rb_raise(rb_eIOError,"concurrent BAM I/O is not supported"); file->active_io=1; }
581
+ typedef struct { ruby_bam_file_t *file; void *(*operation)(void *); void *arguments; } bam_io_call_t;
582
+ static VALUE bam_io_call_without_gvl(VALUE data) { bam_io_call_t *call=(bam_io_call_t *)(uintptr_t)data;rb_thread_call_without_gvl(call->operation,call->arguments,RUBY_UBF_IO,NULL);return Qnil; }
583
+ static VALUE bam_io_end(VALUE data) { bam_io_call_t *call=(bam_io_call_t *)(uintptr_t)data;call->file->active_io=0;return Qnil; }
584
+ /* Async Ruby exceptions are delivered after the native operation regains the GVL.
585
+ * rb_ensure therefore keeps active_io and the stack-backed arguments valid until
586
+ * the HTSlib call has returned or its RUBY_UBF_IO cancellation has completed. */
587
+ static void bam_io_run(ruby_bam_file_t *file,void *(*operation)(void *),void *arguments) { bam_io_call_t call={file,operation,arguments};bam_io_begin(file);rb_ensure(bam_io_call_without_gvl,(VALUE)(uintptr_t)&call,bam_io_end,(VALUE)(uintptr_t)&call); }
544
588
  static VALUE native_bam_read_header(VALUE self) {
545
589
  ruby_bam_file_t *file=get_file(self,0); bam_header_read_args_t args={file->file,NULL};
546
- bam_io_begin(file); rb_thread_call_without_gvl(bam_read_header_without_gvl,&args,RUBY_UBF_IO,NULL); file->active_io=0;
590
+ bam_io_run(file,bam_read_header_without_gvl,&args);
547
591
  return wrap_header(args.result);
548
592
  }
549
593
  static VALUE native_bam_write_header(VALUE self, VALUE header) {
550
594
  ruby_bam_file_t *file=get_file(self,0); bam_io_args_t args={file->file,get_header(header)->pointer,NULL,NULL,0};
551
- bam_io_begin(file); rb_thread_call_without_gvl(bam_write_header_without_gvl,&args,RUBY_UBF_IO,NULL); file->active_io=0; return INT2NUM(args.result);
595
+ bam_io_run(file,bam_write_header_without_gvl,&args); return INT2NUM(args.result);
552
596
  }
553
597
  static VALUE native_bam_read(VALUE self, VALUE header, VALUE record) {
554
598
  ruby_bam_file_t *file=get_file(self,0); bam_io_args_t args={file->file,get_header(header)->pointer,get_record(record)->pointer,NULL,0};
555
- bam_io_begin(file); rb_thread_call_without_gvl(bam_read_without_gvl,&args,RUBY_UBF_IO,NULL); file->active_io=0; return INT2NUM(args.result);
599
+ bam_io_run(file,bam_read_without_gvl,&args); return INT2NUM(args.result);
556
600
  }
557
601
  static VALUE native_bam_write(VALUE self, VALUE header, VALUE record) {
558
602
  ruby_bam_file_t *file=get_file(self,0); bam_io_args_t args={file->file,get_header(header)->pointer,get_record(record)->pointer,NULL,0};
559
- bam_io_begin(file); rb_thread_call_without_gvl(bam_write_without_gvl,&args,RUBY_UBF_IO,NULL); file->active_io=0; return INT2NUM(args.result);
603
+ bam_io_run(file,bam_write_without_gvl,&args); return INT2NUM(args.result);
560
604
  }
561
605
  static VALUE native_bam_set_fai(VALUE self, VALUE path) {
562
606
  return INT2NUM(hts_set_fai_filename(get_file(self, 0)->file, StringValueCStr(path)));
@@ -622,9 +666,7 @@ static VALUE native_bam_iterator_next(VALUE self, VALUE record) {
622
666
  ruby_bam_iterator_t *iterator = get_iterator(self);
623
667
  ruby_bam_file_t *file = get_file(iterator->file, 0);
624
668
  bam_io_args_t args={file->file,NULL,get_record(record)->pointer,iterator->iterator,0};
625
- bam_io_begin(file);
626
- rb_thread_call_without_gvl(bam_iterator_without_gvl,&args,RUBY_UBF_IO,NULL);
627
- file->active_io=0;
669
+ bam_io_run(file,bam_iterator_without_gvl,&args);
628
670
  return INT2NUM(args.result);
629
671
  }
630
672
  static VALUE native_bam_iterator_close(VALUE self) {
@@ -697,24 +739,37 @@ static VALUE base_mod_array(const hts_base_mod *mods, int count) {
697
739
  }
698
740
  return result;
699
741
  }
742
+ static void raise_base_mod_error(const char *operation) {
743
+ VALUE hts = rb_const_get(rb_cObject, rb_intern("HTS"));
744
+ VALUE bam = rb_const_get(hts, rb_intern("Bam"));
745
+ VALUE base_mod = rb_const_get(bam, rb_intern("BaseMod"));
746
+ VALUE error = rb_const_get(base_mod, rb_intern("Error"));
747
+ rb_raise(error, "%s failed", operation);
748
+ }
700
749
  static VALUE native_base_mod_at(VALUE self, VALUE position, VALUE max_value) {
701
750
  ruby_base_mod_t *value = get_base_mod(self);
702
751
  int max = NUM2INT(max_value), count;
703
752
  hts_base_mod *mods;
753
+ VALUE mods_storage = 0;
704
754
  if (max <= 0) rb_raise(rb_eArgError, "max_mods must be positive");
705
- mods = ALLOC_N(hts_base_mod, max);
755
+ mods = ALLOCV_N(hts_base_mod, mods_storage, max);
706
756
  count = bam_mods_at_qpos(get_record(value->record)->pointer, NUM2INT(position), value->state, mods, max);
757
+ if (count < 0) {
758
+ ALLOCV_END(mods_storage);
759
+ raise_base_mod_error("bam_mods_at_qpos");
760
+ }
707
761
  VALUE result = count > 0 ? base_mod_array(mods, count < max ? count : max) : Qnil;
708
- xfree(mods);
762
+ ALLOCV_END(mods_storage);
709
763
  return result;
710
764
  }
711
765
  static VALUE native_base_mod_each_raw(VALUE self, VALUE max_value) {
712
766
  ruby_base_mod_t *value = get_base_mod(self);
713
767
  int max = NUM2INT(max_value), count, position, index;
714
768
  hts_base_mod *mods;
769
+ VALUE mods_storage = 0;
715
770
  if (!rb_block_given_p()) rb_raise(rb_eArgError, "block is required");
716
771
  if (max <= 0) rb_raise(rb_eArgError, "max_mods must be positive");
717
- mods = ALLOC_N(hts_base_mod, max);
772
+ mods = ALLOCV_N(hts_base_mod, mods_storage, max);
718
773
  while ((count = bam_next_basemod(get_record(value->record)->pointer, value->state, mods, max, &position)) > 0) {
719
774
  if (count > max) count = max;
720
775
  for (index = 0; index < count; index++) {
@@ -723,7 +778,11 @@ static VALUE native_base_mod_each_raw(VALUE self, VALUE max_value) {
723
778
  INT2NUM(mods[index].qual));
724
779
  }
725
780
  }
726
- xfree(mods);
781
+ if (count < 0) {
782
+ ALLOCV_END(mods_storage);
783
+ raise_base_mod_error("bam_next_basemod");
784
+ }
785
+ ALLOCV_END(mods_storage);
727
786
  return self;
728
787
  }
729
788
  static VALUE native_base_mod_types(VALUE self) {
@@ -128,7 +128,25 @@ static VALUE wrap_bcf_iterator(VALUE file,VALUE header,hts_itr_t *iterator,int t
128
128
  static VALUE native_bcf_header_create(VALUE klass) { return wrap_bcf_header(bcf_hdr_init("w")); }
129
129
  static VALUE native_bcf_header_duplicate(VALUE self) { return wrap_bcf_header(bcf_hdr_dup(get_bcf_header(self)->pointer)); }
130
130
  static VALUE native_bcf_header_version(VALUE self) { const char *v=bcf_hdr_get_version(get_bcf_header(self)->pointer); return v?rb_str_new_cstr(v):Qnil; }
131
- static VALUE native_bcf_header_set_version(VALUE self,VALUE value) { return INT2NUM(bcf_hdr_set_version(get_bcf_header(self)->pointer,StringValueCStr(value))); }
131
+ static VALUE native_bcf_header_set_version(VALUE self,VALUE value) {
132
+ bcf_hdr_t *header=get_bcf_header(self)->pointer;
133
+ const char *version=StringValueCStr(value);
134
+ #if !defined(HTS_VERSION) || HTS_VERSION < 102200
135
+ /* HTSlib before 1.22 failed to restore the generic-header hash value in
136
+ bcf_hdr_update_hrec(), so calling bcf_hdr_set_version() twice aborted.
137
+ Updating the public header record directly avoids that upstream bug. */
138
+ bcf_hrec_t *record=bcf_hdr_get_hrec(header,BCF_HL_GEN,"fileformat",NULL,NULL);
139
+ if(record) {
140
+ char *copy=strdup(version);
141
+ if(!copy) rb_memerror();
142
+ free(record->value);
143
+ record->value=copy;
144
+ header->dirty=1;
145
+ return INT2NUM(0);
146
+ }
147
+ #endif
148
+ return INT2NUM(bcf_hdr_set_version(header,version));
149
+ }
132
150
  static VALUE native_bcf_header_read_file(VALUE self,VALUE path) { return INT2NUM(bcf_hdr_set(get_bcf_header(self)->pointer,StringValueCStr(path))); }
133
151
  static VALUE native_bcf_header_nsamples(VALUE self) { return INT2NUM(bcf_hdr_nsamples(get_bcf_header(self)->pointer)); }
134
152
  static VALUE native_bcf_header_samples(VALUE self) {
@@ -189,11 +207,12 @@ static VALUE native_bcf_header_schema(VALUE self,VALUE kind_value,VALUE key_valu
189
207
  rb_ary_push(result,INT2NUM(bcf_hdr_id2number(h,kind,id))); rb_ary_push(result,INT2NUM(id)); return result;
190
208
  }
191
209
  static VALUE native_bcf_header_subset(VALUE self,VALUE samples) {
192
- bcf_hdr_t *h=get_bcf_header(self)->pointer,*subset; int count=RARRAY_LEN(samples),i; char **names=NULL; int *imap=NULL; VALUE map;
193
- if(count>0) { names=ALLOC_N(char*,count); imap=ALLOC_N(int,count); for(i=0;i<count;i++) { VALUE sample=rb_ary_entry(samples,i); names[i]=StringValueCStr(sample); } }
194
- subset=bcf_hdr_subset(h,count,names,imap); xfree(names); if(!subset){xfree(imap);return Qnil;}
195
- map=rb_ary_new_capa(count); for(i=0;i<count;i++)rb_ary_push(map,INT2NUM(imap[i])); xfree(imap);
196
- return rb_ary_new_from_args(2,wrap_bcf_header(subset),map);
210
+ bcf_hdr_t *h=get_bcf_header(self)->pointer,*subset; int count=RARRAY_LEN(samples),i; char **names=NULL; int *imap=NULL; VALUE map,names_storage=0,imap_storage=0;
211
+ if(count>0) { names=ALLOCV_N(char*,names_storage,count); imap=ALLOCV_N(int,imap_storage,count); for(i=0;i<count;i++) { VALUE sample=rb_ary_entry(samples,i); names[i]=StringValueCStr(sample); } }
212
+ subset=bcf_hdr_subset(h,count,names,imap); ALLOCV_END(names_storage); if(!subset){ALLOCV_END(imap_storage);return Qnil;}
213
+ VALUE subset_value=wrap_bcf_header(subset);
214
+ map=rb_ary_new_capa(count); for(i=0;i<count;i++)rb_ary_push(map,INT2NUM(imap[i])); ALLOCV_END(imap_storage);
215
+ return rb_ary_new_from_args(2,subset_value,map);
197
216
  }
198
217
 
199
218
  /* Header record */
@@ -204,6 +223,20 @@ static VALUE native_bcf_hrec_find_key(VALUE self,VALUE key) { return INT2NUM(bcf
204
223
  static VALUE native_bcf_hrec_to_s(VALUE self) { kstring_t s=KS_INITIALIZE; VALUE r; bcf_hrec_format(get_bcf_hrec(self)->pointer,&s); r=rb_str_new(s.s,s.l); free(s.s); return r; }
205
224
 
206
225
  /* Record core */
226
+ static void raise_bcf_record_error(const char *message) {
227
+ VALUE hts=rb_const_get(rb_cObject,rb_intern("HTS"));
228
+ VALUE bcf=rb_const_get(hts,rb_intern("Bcf"));
229
+ VALUE error=rb_const_get(bcf,rb_intern("RecordError"));
230
+ rb_raise(error,"%s",message);
231
+ }
232
+ static void check_bcf_unpack(bcf1_t *record,int fields) {
233
+ if(bcf_unpack(record,fields)<0)raise_bcf_record_error("Failed to unpack BCF record");
234
+ }
235
+ static const char *checked_bcf_id(bcf_hdr_t *header,int id) {
236
+ const char *name=(id>=0 && id<header->n[BCF_DT_ID] && header->id[BCF_DT_ID])?bcf_hdr_int2id(header,BCF_DT_ID,id):NULL;
237
+ if(!name)raise_bcf_record_error("BCF record contains an ID that is absent from the supplied header");
238
+ return name;
239
+ }
207
240
  static VALUE native_bcf_record_create(VALUE klass) { return wrap_bcf_record(bcf_init()); }
208
241
  static VALUE native_bcf_record_duplicate(VALUE self) { return wrap_bcf_record(bcf_dup(get_bcf_record(self)->pointer)); }
209
242
  static VALUE native_bcf_record_core_get(VALUE self,VALUE field) {
@@ -221,21 +254,21 @@ static VALUE native_bcf_record_core_set(VALUE self,VALUE field,VALUE value) {
221
254
  else rb_raise(rb_eArgError,"unknown BCF field");
222
255
  return value;
223
256
  }
224
- static VALUE native_bcf_record_id(VALUE self) { bcf1_t *r=get_bcf_record(self)->pointer; bcf_unpack(r,BCF_UN_INFO); return rb_str_new_cstr(r->d.id); }
257
+ static VALUE native_bcf_record_id(VALUE self) { bcf1_t *r=get_bcf_record(self)->pointer; check_bcf_unpack(r,BCF_UN_STR); if(!r->d.id)raise_bcf_record_error("BCF record ID is missing after unpack"); return rb_str_new_cstr(r->d.id); }
225
258
  static VALUE native_bcf_record_set_id(VALUE self,VALUE header,VALUE id) { return INT2NUM(bcf_update_id(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,StringValueCStr(id))); }
226
- static VALUE native_bcf_record_alleles(VALUE self) { bcf1_t *r=get_bcf_record(self)->pointer; int i; VALUE a; bcf_unpack(r,BCF_UN_STR); a=rb_ary_new_capa(r->n_allele); for(i=0;i<r->n_allele;i++)rb_ary_push(a,rb_str_new_cstr(r->d.allele[i])); return a; }
259
+ static VALUE native_bcf_record_alleles(VALUE self) { bcf1_t *r=get_bcf_record(self)->pointer; int i; VALUE a; check_bcf_unpack(r,BCF_UN_STR); a=rb_ary_new_capa(r->n_allele); for(i=0;i<r->n_allele;i++){if(!r->d.allele[i])raise_bcf_record_error("BCF allele is missing after unpack");rb_ary_push(a,rb_str_new_cstr(r->d.allele[i]));} return a; }
227
260
  static VALUE native_bcf_record_set_alleles(VALUE self,VALUE header,VALUE value) { return INT2NUM(bcf_update_alleles_str(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,StringValueCStr(value))); }
228
- static VALUE native_bcf_record_filter_ids(VALUE self) { bcf1_t *r=get_bcf_record(self)->pointer; int i; VALUE a; bcf_unpack(r,BCF_UN_FLT); a=rb_ary_new_capa(r->d.n_flt); for(i=0;i<r->d.n_flt;i++)rb_ary_push(a,INT2NUM(r->d.flt[i])); return a; }
229
- static VALUE native_bcf_record_filter_names(VALUE self,VALUE header) { bcf1_t *r=get_bcf_record(self)->pointer; bcf_hdr_t *h=get_bcf_header(header)->pointer; int i; VALUE a; bcf_unpack(r,BCF_UN_FLT); a=rb_ary_new_capa(r->d.n_flt); for(i=0;i<r->d.n_flt;i++)rb_ary_push(a,rb_str_new_cstr(bcf_hdr_int2id(h,BCF_DT_ID,r->d.flt[i]))); return a; }
261
+ static VALUE native_bcf_record_filter_ids(VALUE self) { bcf1_t *r=get_bcf_record(self)->pointer; int i; VALUE a; check_bcf_unpack(r,BCF_UN_FLT); a=rb_ary_new_capa(r->d.n_flt); for(i=0;i<r->d.n_flt;i++)rb_ary_push(a,INT2NUM(r->d.flt[i])); return a; }
262
+ static VALUE native_bcf_record_filter_names(VALUE self,VALUE header) { bcf1_t *r=get_bcf_record(self)->pointer; bcf_hdr_t *h=get_bcf_header(header)->pointer; int i; VALUE a; check_bcf_unpack(r,BCF_UN_FLT); a=rb_ary_new_capa(r->d.n_flt); for(i=0;i<r->d.n_flt;i++)rb_ary_push(a,rb_str_new_cstr(checked_bcf_id(h,r->d.flt[i]))); return a; }
230
263
  static VALUE native_bcf_record_to_s(VALUE self,VALUE header) { kstring_t s=KS_INITIALIZE; VALUE v; if(vcf_format(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,&s)<0){free(s.s);rb_raise(rb_eRuntimeError,"failed to format BCF record");}v=rb_str_new(s.s,s.l);free(s.s);return v; }
231
264
  static VALUE native_bcf_record_set_unpack(VALUE self,VALUE level){get_bcf_record(self)->pointer->max_unpack=NUM2INT(level);return level;}
232
- static VALUE native_bcf_record_subset(VALUE self,VALUE header,VALUE map){int count=RARRAY_LEN(map),i,*imap=ALLOC_N(int,count?count:1);for(i=0;i<count;i++)imap[i]=NUM2INT(rb_ary_entry(map,i));int result=bcf_subset(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,count,imap);xfree(imap);return INT2NUM(result);}
265
+ static VALUE native_bcf_record_subset(VALUE self,VALUE header,VALUE map){VALUE storage=0;int count=RARRAY_LEN(map),i,*imap=ALLOCV_N(int,storage,count?count:1);for(i=0;i<count;i++)imap[i]=NUM2INT(rb_ary_entry(map,i));int result=bcf_subset(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,count,imap);ALLOCV_END(storage);return INT2NUM(result);}
233
266
 
234
267
  static VALUE field_rows(bcf_hdr_t *h,bcf1_t *r,int format) {
235
- int i,count; VALUE rows; if(format){bcf_unpack(r,BCF_UN_FMT);count=r->n_fmt;}else{bcf_unpack(r,BCF_UN_INFO);count=r->n_info;}
268
+ int i,count; VALUE rows; if(format){check_bcf_unpack(r,BCF_UN_FMT);count=r->n_fmt;}else{check_bcf_unpack(r,BCF_UN_INFO);count=r->n_info;}
236
269
  rows=rb_ary_new_capa(count);
237
270
  for(i=0;i<count;i++) { int id=format?r->d.fmt[i].id:r->d.info[i].key, kind=format?BCF_HL_FMT:BCF_HL_INFO; VALUE row=rb_hash_new();
238
- rb_hash_aset(row,ID2SYM(rb_intern("name")),rb_str_new_cstr(bcf_hdr_int2id(h,BCF_DT_ID,id)));
271
+ rb_hash_aset(row,ID2SYM(rb_intern("name")),rb_str_new_cstr(checked_bcf_id(h,id)));
239
272
  rb_hash_aset(row,ID2SYM(rb_intern("n")),INT2NUM(bcf_hdr_id2number(h,kind,id)));
240
273
  rb_hash_aset(row,ID2SYM(rb_intern("type")),type_symbol(bcf_hdr_id2type(h,kind,id)));
241
274
  rb_hash_aset(row,ID2SYM(rb_intern(format?"id":"key")),INT2NUM(id)); rb_ary_push(rows,row); }
@@ -244,82 +277,120 @@ static VALUE field_rows(bcf_hdr_t *h,bcf1_t *r,int format) {
244
277
  static VALUE native_bcf_record_info_fields(VALUE self,VALUE header) { return field_rows(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,0); }
245
278
  static VALUE native_bcf_record_format_fields(VALUE self,VALUE header) { return field_rows(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,1); }
246
279
 
247
- static VALUE info_get(VALUE self,VALUE header_value,VALUE key_value,VALUE type_value) {
248
- bcf_hdr_t *h=get_bcf_header(header_value)->pointer; bcf1_t *r=get_bcf_record(self)->pointer; int type=NUM2INT(type_value),cap=0,count,i; void *dst=NULL; VALUE value;
249
- count=bcf_get_info_values(h,r,StringValueCStr(key_value),&dst,&cap,type);
250
- if(count<0){free(dst);return Qnil;}
251
- if(type==BCF_HT_FLAG){free(dst);return count==1?Qtrue:Qnil;}
252
- if(type==BCF_HT_STR)value=rb_str_new_cstr((char*)dst);
253
- else { value=rb_ary_new_capa(count); for(i=0;i<count;i++) {
254
- if(type==BCF_HT_INT){
255
- int32_t item=((int32_t*)dst)[i];
280
+ typedef struct { void *dst; int count; int type; } bcf_value_result_t;
281
+ static void raise_bcf_field_read_error(const char *kind,const char *key,int code) {
282
+ VALUE hts=rb_const_get(rb_cObject,rb_intern("HTS"));
283
+ VALUE bcf=rb_const_get(hts,rb_intern("Bcf"));
284
+ VALUE error=rb_const_get(bcf,rb_intern(kind));
285
+ const char *reason;
286
+ switch(code){case -1:reason="tag is not defined in the header";break;case -2:reason="stored type does not match the requested type";break;case -4:reason="native allocation failed";break;default:reason="native read failed";break;}
287
+ rb_raise(error,"Failed to read %s (HTSlib error %d: %s)",key,code,reason);
288
+ }
289
+ static VALUE bcf_value_result_free(VALUE data) {
290
+ bcf_value_result_t *result=(bcf_value_result_t *)(uintptr_t)data;
291
+ free(result->dst); result->dst=NULL; return Qnil;
292
+ }
293
+ static VALUE bcf_info_result_to_ruby(VALUE data) {
294
+ bcf_value_result_t *result=(bcf_value_result_t *)(uintptr_t)data; int i; VALUE value;
295
+ if(result->type==BCF_HT_STR)return rb_str_new_cstr((char*)result->dst);
296
+ value=rb_ary_new_capa(result->count); for(i=0;i<result->count;i++) {
297
+ if(result->type==BCF_HT_INT){
298
+ int32_t item=((int32_t*)result->dst)[i];
256
299
  if(item==bcf_int32_vector_end)break;
257
300
  rb_ary_push(value,item==bcf_int32_missing?Qnil:INT2NUM(item));
258
301
  }
259
- else if(type==BCF_HT_LONG){
260
- int64_t item=((int64_t*)dst)[i];
302
+ else if(result->type==BCF_HT_LONG){
303
+ int64_t item=((int64_t*)result->dst)[i];
261
304
  if(item==bcf_int64_vector_end)break;
262
305
  rb_ary_push(value,item==bcf_int64_missing?Qnil:LL2NUM(item));
263
306
  }
264
307
  else {
265
- float item=((float*)dst)[i];
308
+ float item=((float*)result->dst)[i];
266
309
  if(bcf_float_is_vector_end(item))break;
267
310
  rb_ary_push(value,bcf_float_is_missing(item)?Qnil:DBL2NUM(item));
268
- } } }
269
- free(dst);return value;
311
+ }
312
+ }
313
+ return value;
314
+ }
315
+ static VALUE info_get(VALUE self,VALUE header_value,VALUE key_value,VALUE type_value) {
316
+ bcf_hdr_t *h=get_bcf_header(header_value)->pointer; bcf1_t *r=get_bcf_record(self)->pointer; int type=NUM2INT(type_value),cap=0,count; void *dst=NULL; bcf_value_result_t result;
317
+ count=bcf_get_info_values(h,r,StringValueCStr(key_value),&dst,&cap,type);
318
+ if(count<0){const char *key=StringValueCStr(key_value);free(dst);if(count==-3)return Qnil;raise_bcf_field_read_error("InfoReadError",key,count);}
319
+ if(type==BCF_HT_FLAG){free(dst);return count==1?Qtrue:Qnil;}
320
+ result.dst=dst;result.count=count;result.type=type;
321
+ return rb_ensure(bcf_info_result_to_ruby,(VALUE)(uintptr_t)&result,
322
+ bcf_value_result_free,(VALUE)(uintptr_t)&result);
270
323
  }
271
324
  static VALUE native_bcf_info_get(VALUE self,VALUE header,VALUE key,VALUE type) { return info_get(self,header,key,type); }
272
325
  static VALUE native_bcf_info_present(VALUE self,VALUE header,VALUE key,VALUE type) { return NIL_P(info_get(self,header,key,type))?Qfalse:Qtrue; }
273
- static int fill_i32(VALUE array,int32_t **out) { int n=RARRAY_LEN(array),i; *out=ALLOC_N(int32_t,n?n:1);for(i=0;i<n;i++)(*out)[i]=NUM2INT(rb_ary_entry(array,i));return n; }
274
- static int fill_float(VALUE array,float **out) { int n=RARRAY_LEN(array),i; *out=ALLOC_N(float,n?n:1);for(i=0;i<n;i++)(*out)[i]=(float)NUM2DBL(rb_ary_entry(array,i));return n; }
275
326
  static VALUE native_bcf_info_update(VALUE self,VALUE header,VALUE key,VALUE type_value,VALUE values) {
276
- bcf_hdr_t*h=get_bcf_header(header)->pointer;bcf1_t*r=get_bcf_record(self)->pointer;int type=NUM2INT(type_value),n,result;void*data=NULL;
327
+ bcf_hdr_t*h=get_bcf_header(header)->pointer;bcf1_t*r=get_bcf_record(self)->pointer;int type=NUM2INT(type_value),n,i,result;void*data=NULL;VALUE storage=0;
277
328
  if(type!=BCF_HT_FLAG && (NIL_P(values) || values==Qfalse)){
278
329
  return INT2NUM(bcf_update_info(h,r,StringValueCStr(key),NULL,0,type));
279
330
  }
280
- if(type==BCF_HT_INT){int32_t*p;n=fill_i32(values,&p);data=p;result=bcf_update_info(h,r,StringValueCStr(key),data,n,type);xfree(p);}
281
- else if(type==BCF_HT_REAL){float*p;n=fill_float(values,&p);data=p;result=bcf_update_info(h,r,StringValueCStr(key),data,n,type);xfree(p);}
331
+ if(type==BCF_HT_INT){int32_t*p;n=RARRAY_LEN(values);p=ALLOCV_N(int32_t,storage,n?n:1);for(i=0;i<n;i++)p[i]=NUM2INT(rb_ary_entry(values,i));data=p;result=bcf_update_info(h,r,StringValueCStr(key),data,n,type);ALLOCV_END(storage);}
332
+ else if(type==BCF_HT_REAL){float*p;n=RARRAY_LEN(values);p=ALLOCV_N(float,storage,n?n:1);for(i=0;i<n;i++)p[i]=(float)NUM2DBL(rb_ary_entry(values,i));data=p;result=bcf_update_info(h,r,StringValueCStr(key),data,n,type);ALLOCV_END(storage);}
282
333
  else if(type==BCF_HT_STR){result=bcf_update_info(h,r,StringValueCStr(key),StringValueCStr(values),1,type);}
283
334
  else { n=RTEST(values)?1:0; result=bcf_update_info(h,r,StringValueCStr(key),NULL,n,type); }
284
335
  return INT2NUM(result);
285
336
  }
286
337
 
338
+ typedef struct { char **strings; int sample_count; } bcf_string_result_t;
339
+ static VALUE bcf_string_result_free(VALUE data) {
340
+ bcf_string_result_t *result=(bcf_string_result_t *)(uintptr_t)data;
341
+ if(result->strings){if(result->sample_count>0)free(result->strings[0]);free(result->strings);result->strings=NULL;}
342
+ return Qnil;
343
+ }
344
+ static VALUE bcf_string_result_to_ruby(VALUE data) {
345
+ bcf_string_result_t *result=(bcf_string_result_t *)(uintptr_t)data; int i;
346
+ VALUE value=rb_ary_new_capa(result->sample_count);
347
+ for(i=0;i<result->sample_count;i++)rb_ary_push(value,rb_str_new_cstr(result->strings[i]));
348
+ return value;
349
+ }
350
+ typedef struct { void *dst; int count; int type; VALUE raw; } bcf_format_result_t;
351
+ static VALUE bcf_format_result_to_ruby(VALUE data) {
352
+ bcf_format_result_t *result=(bcf_format_result_t *)(uintptr_t)data; int i; VALUE value=rb_ary_new_capa(result->count);
353
+ for(i=0;i<result->count;i++){if(result->type==BCF_HT_INT)rb_ary_push(value,INT2NUM(((int32_t*)result->dst)[i]));else {float f=((float*)result->dst)[i];uint32_t word;memcpy(&word,&f,4);if(RTEST(result->raw))rb_ary_push(value,UINT2NUM(word));else if(bcf_float_is_missing(f)||bcf_float_is_vector_end(f))rb_ary_push(value,Qnil);else rb_ary_push(value,DBL2NUM(f));}}
354
+ return value;
355
+ }
287
356
  static VALUE native_bcf_format_get(VALUE self,VALUE header_value,VALUE key_value,VALUE type_value,VALUE raw_value) {
288
- bcf_hdr_t*h=get_bcf_header(header_value)->pointer;bcf1_t*r=get_bcf_record(self)->pointer;int type=NUM2INT(type_value),cap=0,count,i;void*dst=NULL;VALUE value;
357
+ bcf_hdr_t*h=get_bcf_header(header_value)->pointer;bcf1_t*r=get_bcf_record(self)->pointer;int type=NUM2INT(type_value),cap=0,count;void*dst=NULL;
289
358
  if(type==BCF_HT_STR){
290
359
  char **strings=NULL;
291
360
  int sample_count=bcf_hdr_nsamples(h);
361
+ bcf_string_result_t result;
292
362
  count=bcf_get_format_string(h,r,StringValueCStr(key_value),&strings,&cap);
293
- if(count<0){free(strings);return Qnil;}
294
- value=rb_ary_new_capa(sample_count);
295
- for(i=0;i<sample_count;i++)rb_ary_push(value,rb_str_new_cstr(strings[i]));
296
- if(strings){if(sample_count>0)free(strings[0]);free(strings);}
297
- return value;
363
+ if(count<0){const char *key=StringValueCStr(key_value);free(strings);if(count==-3)return Qnil;raise_bcf_field_read_error("FormatReadError",key,count);}
364
+ result.strings=strings;result.sample_count=sample_count;
365
+ return rb_ensure(bcf_string_result_to_ruby,(VALUE)(uintptr_t)&result,
366
+ bcf_string_result_free,(VALUE)(uintptr_t)&result);
298
367
  }
299
- count=bcf_get_format_values(h,r,StringValueCStr(key_value),&dst,&cap,type);if(count<0){free(dst);return Qnil;}value=rb_ary_new_capa(count);
300
- for(i=0;i<count;i++){if(type==BCF_HT_INT)rb_ary_push(value,INT2NUM(((int32_t*)dst)[i]));else {float f=((float*)dst)[i];uint32_t word;memcpy(&word,&f,4);if(RTEST(raw_value))rb_ary_push(value,UINT2NUM(word));else if(bcf_float_is_missing(f)||bcf_float_is_vector_end(f))rb_ary_push(value,Qnil);else rb_ary_push(value,DBL2NUM(f));}}
301
- free(dst);return value;
368
+ count=bcf_get_format_values(h,r,StringValueCStr(key_value),&dst,&cap,type);if(count<0){const char *key=StringValueCStr(key_value);free(dst);if(count==-3)return Qnil;raise_bcf_field_read_error("FormatReadError",key,count);}
369
+ bcf_format_result_t result={dst,count,type,raw_value};
370
+ return rb_ensure(bcf_format_result_to_ruby,(VALUE)(uintptr_t)&result,
371
+ bcf_value_result_free,(VALUE)(uintptr_t)&result);
302
372
  }
303
373
  static VALUE native_bcf_format_update(VALUE self,VALUE header,VALUE key,VALUE type_value,VALUE values) {
304
- bcf_hdr_t*h=get_bcf_header(header)->pointer;bcf1_t*r=get_bcf_record(self)->pointer;int type=NUM2INT(type_value),n,result;
305
- if(type==BCF_HT_INT){int32_t*p;n=fill_i32(values,&p);result=bcf_update_format_int32(h,r,StringValueCStr(key),p,n);xfree(p);}
306
- else if(type==BCF_HT_REAL){float*p;n=fill_float(values,&p);result=bcf_update_format_float(h,r,StringValueCStr(key),p,n);xfree(p);}
307
- else {int i;char**strings;n=RARRAY_LEN(values);strings=ALLOC_N(char*,n?n:1);for(i=0;i<n;i++){VALUE string=rb_ary_entry(values,i);strings[i]=StringValueCStr(string);}result=bcf_update_format_string(h,r,StringValueCStr(key),(const char**)strings,n);xfree(strings);}return INT2NUM(result);
374
+ bcf_hdr_t*h=get_bcf_header(header)->pointer;bcf1_t*r=get_bcf_record(self)->pointer;int type=NUM2INT(type_value),n,i,result;VALUE storage=0;
375
+ if(type==BCF_HT_INT){int32_t*p;n=RARRAY_LEN(values);p=ALLOCV_N(int32_t,storage,n?n:1);for(i=0;i<n;i++)p[i]=NUM2INT(rb_ary_entry(values,i));result=bcf_update_format_int32(h,r,StringValueCStr(key),p,n);ALLOCV_END(storage);}
376
+ else if(type==BCF_HT_REAL){float*p;n=RARRAY_LEN(values);p=ALLOCV_N(float,storage,n?n:1);for(i=0;i<n;i++)p[i]=(float)NUM2DBL(rb_ary_entry(values,i));result=bcf_update_format_float(h,r,StringValueCStr(key),p,n);ALLOCV_END(storage);}
377
+ else {int i;char**strings;n=RARRAY_LEN(values);strings=ALLOCV_N(char*,storage,n?n:1);for(i=0;i<n;i++){VALUE string=rb_ary_entry(values,i);strings[i]=StringValueCStr(string);}result=bcf_update_format_string(h,r,StringValueCStr(key),(const char**)strings,n);ALLOCV_END(storage);}return INT2NUM(result);
308
378
  }
309
379
  static VALUE native_bcf_format_update_float_words(VALUE self,VALUE header,VALUE key,VALUE values) {
310
380
  bcf_hdr_t *h=get_bcf_header(header)->pointer;
311
381
  bcf1_t *r=get_bcf_record(self)->pointer;
312
382
  int n=RARRAY_LEN(values),i,result;
313
- float *floats=ALLOC_N(float,n?n:1);
383
+ VALUE storage=0;
384
+ float *floats=ALLOCV_N(float,storage,n?n:1);
314
385
  for(i=0;i<n;i++){
315
386
  uint32_t word=NUM2UINT(rb_ary_entry(values,i));
316
387
  memcpy(&floats[i],&word,sizeof(word));
317
388
  }
318
389
  result=bcf_update_format_float(h,r,StringValueCStr(key),floats,n);
319
- xfree(floats);
390
+ ALLOCV_END(storage);
320
391
  return INT2NUM(result);
321
392
  }
322
- static VALUE native_bcf_genotype_update(VALUE self,VALUE header,VALUE values) {int32_t*p;int n=fill_i32(values,&p);int result=bcf_update_genotypes(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,p,n);xfree(p);return INT2NUM(result);}
393
+ static VALUE native_bcf_genotype_update(VALUE self,VALUE header,VALUE values) {VALUE storage=0;int32_t*p;int n=RARRAY_LEN(values),i;p=ALLOCV_N(int32_t,storage,n?n:1);for(i=0;i<n;i++)p[i]=NUM2INT(rb_ary_entry(values,i));int result=bcf_update_genotypes(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,p,n);ALLOCV_END(storage);return INT2NUM(result);}
323
394
  static VALUE native_bcf_format_delete(VALUE self,VALUE header,VALUE key,VALUE type) {return INT2NUM(bcf_update_format(get_bcf_header(header)->pointer,get_bcf_record(self)->pointer,StringValueCStr(key),NULL,0,NUM2INT(type)));}
324
395
 
325
396
  /* File */
@@ -331,13 +402,20 @@ static void *bcf_write_header_without_gvl(void*data){bcf_io_args_t*a=data;a->res
331
402
  static void *bcf_write_without_gvl(void*data){bcf_io_args_t*a=data;a->result=bcf_write(a->file,a->header,a->record);return NULL;}
332
403
  static void *bcf_iterator_without_gvl(void*data){bcf_io_args_t*a=data;if(a->tabix){int n=tbx_itr_next(a->file,a->tabix,a->iterator,a->line);a->result=n<0?n:vcf_parse(a->line,a->header,a->record);}else a->result=bcf_itr_next(a->file,a->iterator,a->record);return NULL;}
333
404
  static void bcf_io_begin(ruby_bcf_file_t*v){if(v->active_io)rb_raise(rb_eIOError,"concurrent BCF I/O is not supported");v->active_io=1;}
334
- static VALUE native_bcf_file_open(VALUE klass,VALUE path,VALUE mode) {ruby_bcf_file_t*v;VALUE o=TypedData_Make_Struct(klass,ruby_bcf_file_t,&bcf_file_type,v);v->index=NULL;v->tabix=NULL;v->active_io=0;v->path=rb_str_dup(StringValue(path));RB_OBJ_WRITE(o,&v->path,v->path);v->file=hts_open(StringValueCStr(v->path),StringValueCStr(mode));if(!v->file)rb_syserr_fail_str(ENOENT,path);return o;}
335
- static VALUE native_bcf_file_close(VALUE self){ruby_bcf_file_t*v=get_bcf_file(self,1);if(v->active_io)rb_raise(rb_eIOError,"cannot close BCF during active I/O");if(v->index){hts_idx_destroy(v->index);v->index=NULL;}if(v->tabix){tbx_destroy(v->tabix);v->tabix=NULL;}if(v->file){hts_close(v->file);v->file=NULL;}return Qnil;}
405
+ typedef struct {ruby_bcf_file_t*file;void*(*operation)(void*);void*arguments;} bcf_io_call_t;
406
+ static VALUE bcf_io_call_without_gvl(VALUE data){bcf_io_call_t*call=(bcf_io_call_t*)(uintptr_t)data;rb_thread_call_without_gvl(call->operation,call->arguments,RUBY_UBF_IO,NULL);return Qnil;}
407
+ static VALUE bcf_io_end(VALUE data){bcf_io_call_t*call=(bcf_io_call_t*)(uintptr_t)data;call->file->active_io=0;return Qnil;}
408
+ /* Async exceptions run only after the no-GVL operation (and any UBF cancellation)
409
+ * returns; rb_ensure resets the guard while the call arguments are still alive. */
410
+ static void bcf_io_run(ruby_bcf_file_t*file,void*(*operation)(void*),void*arguments){bcf_io_call_t call={file,operation,arguments};bcf_io_begin(file);rb_ensure(bcf_io_call_without_gvl,(VALUE)(uintptr_t)&call,bcf_io_end,(VALUE)(uintptr_t)&call);}
411
+ static void raise_bcf_open_error(VALUE path,int error){if(error)rb_syserr_fail_str(error,path);VALUE hts=rb_const_get(rb_cObject,rb_intern("HTS"));VALUE bcf=rb_const_get(hts,rb_intern("Bcf"));VALUE klass=rb_const_get(bcf,rb_intern("OpenError"));rb_raise(klass,"Failed to open %"PRIsVALUE": HTSlib could not recognize or open the input",path);}
412
+ static VALUE native_bcf_file_open(VALUE klass,VALUE path,VALUE mode) {ruby_bcf_file_t*v;VALUE o=TypedData_Make_Struct(klass,ruby_bcf_file_t,&bcf_file_type,v);v->index=NULL;v->tabix=NULL;v->active_io=0;v->path=rb_str_dup(StringValue(path));RB_OBJ_WRITE(o,&v->path,v->path);errno=0;v->file=hts_open(StringValueCStr(v->path),StringValueCStr(mode));if(!v->file)raise_bcf_open_error(path,errno);return o;}
413
+ static VALUE native_bcf_file_close(VALUE self){ruby_bcf_file_t*v=get_bcf_file(self,1);int result=0;if(v->active_io)rb_raise(rb_eIOError,"cannot close BCF during active I/O");if(v->index){hts_idx_destroy(v->index);v->index=NULL;}if(v->tabix){tbx_destroy(v->tabix);v->tabix=NULL;}if(v->file){result=hts_close(v->file);v->file=NULL;}return INT2NUM(result);}
336
414
  static VALUE native_bcf_file_closed(VALUE self){return get_bcf_file(self,1)->file?Qfalse:Qtrue;}
337
- static VALUE native_bcf_file_read_header(VALUE self){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_header_read_args_t a={v->file,NULL};bcf_io_begin(v);rb_thread_call_without_gvl(bcf_read_header_without_gvl,&a,RUBY_UBF_IO,NULL);v->active_io=0;return wrap_bcf_header(a.result);}
338
- static VALUE native_bcf_file_read(VALUE self,VALUE header,VALUE record){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_io_args_t a={v->file,get_bcf_header(header)->pointer,get_bcf_record(record)->pointer,NULL,NULL,NULL,0};bcf_io_begin(v);rb_thread_call_without_gvl(bcf_read_without_gvl,&a,RUBY_UBF_IO,NULL);v->active_io=0;return INT2NUM(a.result);}
339
- static VALUE native_bcf_file_write_header(VALUE self,VALUE header){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_io_args_t a={v->file,get_bcf_header(header)->pointer,NULL,NULL,NULL,NULL,0};bcf_io_begin(v);rb_thread_call_without_gvl(bcf_write_header_without_gvl,&a,RUBY_UBF_IO,NULL);v->active_io=0;return INT2NUM(a.result);}
340
- static VALUE native_bcf_file_write(VALUE self,VALUE header,VALUE record){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_io_args_t a={v->file,get_bcf_header(header)->pointer,get_bcf_record(record)->pointer,NULL,NULL,NULL,0};bcf_io_begin(v);rb_thread_call_without_gvl(bcf_write_without_gvl,&a,RUBY_UBF_IO,NULL);v->active_io=0;return INT2NUM(a.result);}
415
+ static VALUE native_bcf_file_read_header(VALUE self){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_header_read_args_t a={v->file,NULL};bcf_io_run(v,bcf_read_header_without_gvl,&a);return wrap_bcf_header(a.result);}
416
+ static VALUE native_bcf_file_read(VALUE self,VALUE header,VALUE record){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_io_args_t a={v->file,get_bcf_header(header)->pointer,get_bcf_record(record)->pointer,NULL,NULL,NULL,0};bcf_io_run(v,bcf_read_without_gvl,&a);return INT2NUM(a.result);}
417
+ static VALUE native_bcf_file_write_header(VALUE self,VALUE header){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_io_args_t a={v->file,get_bcf_header(header)->pointer,NULL,NULL,NULL,NULL,0};bcf_io_run(v,bcf_write_header_without_gvl,&a);return INT2NUM(a.result);}
418
+ static VALUE native_bcf_file_write(VALUE self,VALUE header,VALUE record){ruby_bcf_file_t*v=get_bcf_file(self,0);bcf_io_args_t a={v->file,get_bcf_header(header)->pointer,get_bcf_record(record)->pointer,NULL,NULL,NULL,0};bcf_io_run(v,bcf_write_without_gvl,&a);return INT2NUM(a.result);}
341
419
  static VALUE native_bcf_file_set_threads(VALUE self,VALUE n){return INT2NUM(hts_set_threads(get_bcf_file(self,0)->file,NUM2INT(n)));}
342
420
  static VALUE native_bcf_file_format(VALUE self){const htsFormat*f=hts_get_format(get_bcf_file(self,0)->file);if(!f)return Qnil;switch(f->format){case vcf:return rb_str_new_cstr("vcf");case bcf:return rb_str_new_cstr("bcf");default:return rb_str_new_cstr("unknown_format");}}
343
421
  static VALUE native_bcf_file_version(VALUE self){const htsFormat*f=hts_get_format(get_bcf_file(self,0)->file);if(!f)return Qnil;if(f->version.minor==-1)return rb_sprintf("%d",f->version.major);return rb_sprintf("%d.%d",f->version.major,f->version.minor);}
@@ -348,7 +426,7 @@ static VALUE native_bcf_file_index_loaded(VALUE self){ruby_bcf_file_t*v=get_bcf_
348
426
  static VALUE native_bcf_file_build_index(VALUE klass,VALUE path,VALUE index,VALUE shift,VALUE threads){return INT2NUM(bcf_index_build3(StringValueCStr(path),NIL_P(index)?NULL:StringValueCStr(index),NUM2INT(shift),NUM2INT(threads)));}
349
427
  static VALUE native_bcf_file_query_region(VALUE self,VALUE header,VALUE region){ruby_bcf_file_t*v=get_bcf_file(self,0);hts_itr_t*i;if(v->tabix)i=tbx_itr_querys(v->tabix,StringValueCStr(region));else i=bcf_itr_querys(v->index,get_bcf_header(header)->pointer,StringValueCStr(region));return wrap_bcf_iterator(self,header,i,v->tabix!=NULL);}
350
428
  static VALUE native_bcf_file_query_interval(VALUE self,VALUE header,VALUE rid,VALUE beg,VALUE end){ruby_bcf_file_t*v=get_bcf_file(self,0);hts_itr_t*i=v->tabix?tbx_itr_queryi(v->tabix,NUM2INT(rid),NUM2LL(beg),NUM2LL(end)):bcf_itr_queryi(v->index,NUM2INT(rid),NUM2LL(beg),NUM2LL(end));return wrap_bcf_iterator(self,header,i,v->tabix!=NULL);}
351
- static VALUE native_bcf_iterator_next(VALUE self,VALUE record){ruby_bcf_iterator_t*i=get_bcf_iterator(self);ruby_bcf_file_t*f=get_bcf_file(i->file,0);bcf_io_args_t a={f->file,get_bcf_header(i->header)->pointer,get_bcf_record(record)->pointer,i->iterator,i->tabix?f->tabix:NULL,&i->line,0};bcf_io_begin(f);rb_thread_call_without_gvl(bcf_iterator_without_gvl,&a,RUBY_UBF_IO,NULL);f->active_io=0;return INT2NUM(a.result);}
429
+ static VALUE native_bcf_iterator_next(VALUE self,VALUE record){ruby_bcf_iterator_t*i=get_bcf_iterator(self);ruby_bcf_file_t*f=get_bcf_file(i->file,0);bcf_io_args_t a={f->file,get_bcf_header(i->header)->pointer,get_bcf_record(record)->pointer,i->iterator,i->tabix?f->tabix:NULL,&i->line,0};bcf_io_run(f,bcf_iterator_without_gvl,&a);return INT2NUM(a.result);}
352
430
  static VALUE native_bcf_iterator_close(VALUE self){ruby_bcf_iterator_t*v;TypedData_Get_Struct(self,ruby_bcf_iterator_t,&bcf_iterator_type,v);if(v->iterator){hts_itr_destroy(v->iterator);v->iterator=NULL;}return Qnil;}
353
431
 
354
432
  void Init_htslib_native_bcf(VALUE native) {
@@ -1,6 +1,7 @@
1
1
  #include "htslib_native.h"
2
2
 
3
3
  #include <htslib/faidx.h>
4
+ #include <errno.h>
4
5
  #include <stdlib.h>
5
6
 
6
7
  typedef struct {
@@ -47,10 +48,18 @@ static VALUE native_faidx_open(VALUE klass, VALUE path_value, VALUE format_value
47
48
  enum fai_format_options format = NUM2INT(format_value) == 1 ? FAI_FASTQ : FAI_FASTA;
48
49
 
49
50
  object = TypedData_Make_Struct(klass, ruby_faidx_t, &ruby_faidx_type, handle);
51
+ errno = 0;
50
52
  handle->fai = RTEST(auto_build_value)
51
53
  ? fai_load_format(path, format)
52
54
  : fai_load3_format(path, NULL, NULL, 0, format);
53
- if (handle->fai == NULL) rb_syserr_fail_str(ENOENT, path_value);
55
+ if (handle->fai == NULL) {
56
+ int error = errno;
57
+ if (error) rb_syserr_fail_str(error, path_value);
58
+ VALUE hts = rb_const_get(rb_cObject, rb_intern("HTS"));
59
+ VALUE faidx = rb_const_get(hts, rb_intern("Faidx"));
60
+ VALUE open_error = rb_const_get(faidx, rb_intern("OpenError"));
61
+ rb_raise(open_error, "Failed to open %"PRIsVALUE": HTSlib could not load the index", path_value);
62
+ }
54
63
  return object;
55
64
  }
56
65
 
@@ -3,6 +3,7 @@
3
3
  #include <htslib/hts.h>
4
4
  #include <htslib/kstring.h>
5
5
  #include <htslib/tbx.h>
6
+ #include <errno.h>
6
7
  #include <stdlib.h>
7
8
 
8
9
  typedef struct {
@@ -72,8 +73,16 @@ static VALUE native_tabix_open(VALUE klass, VALUE path_value)
72
73
  handle->active_queries = 0;
73
74
  handle->path = rb_str_dup(StringValue(path_value));
74
75
  RB_OBJ_WRITE(object, &handle->path, handle->path);
76
+ errno = 0;
75
77
  handle->file = hts_open(StringValueCStr(handle->path), "r");
76
- if (handle->file == NULL) rb_syserr_fail_str(ENOENT, path_value);
78
+ if (handle->file == NULL) {
79
+ int error = errno;
80
+ if (error) rb_syserr_fail_str(error, path_value);
81
+ VALUE hts = rb_const_get(rb_cObject, rb_intern("HTS"));
82
+ VALUE tabix = rb_const_get(hts, rb_intern("Tabix"));
83
+ VALUE open_error = rb_const_get(tabix, rb_intern("OpenError"));
84
+ rb_raise(open_error, "Failed to open %"PRIsVALUE": HTSlib could not recognize or open the input", path_value);
85
+ }
77
86
  return object;
78
87
  }
79
88
 
@@ -12,6 +12,7 @@ module HTS
12
12
  class BaseMod
13
13
  include Enumerable
14
14
 
15
+ class Error < HTS::Error; end
15
16
  class NotParsedError < StandardError; end
16
17
 
17
18
  attr_reader :record
data/lib/hts/bam/flag.rb CHANGED
@@ -28,6 +28,10 @@ module HTS
28
28
  DUPLICATE = 1024
29
29
  SUPPLEMENTARY = 2048
30
30
 
31
+ UNMAP = UNMAPPED
32
+ MUNMAP = MATE_UNMAPPED
33
+ DUP = DUPLICATE
34
+
31
35
  TABLE = { paired?: PAIRED, proper_pair?: PROPER_PAIR, unmapped?: UNMAPPED,
32
36
  mate_unmapped?: MATE_UNMAPPED, reverse?: REVERSE,
33
37
  mate_reverse?: MATE_REVERSE, read1?: READ1, read2?: READ2,
@@ -74,9 +78,7 @@ module HTS
74
78
  end
75
79
 
76
80
  def ~
77
- # FIXME: Only 12bits are used for flags
78
- # The result is different from the Crystal version.
79
- self.class.new(~@value)
81
+ self.class.new((~@value) & 0x0fff)
80
82
  end
81
83
 
82
84
  def <<(f)
data/lib/hts/bam.rb CHANGED
@@ -17,6 +17,11 @@ module HTS
17
17
  class Bam
18
18
  include Enumerable
19
19
 
20
+ class ReadError < HTS::Error; end
21
+ class WriteError < HTS::Error; end
22
+ class OpenError < HTS::Error; end
23
+ class MissingIndexError < HTS::Error; end
24
+
20
25
  # Filter an owning batch of records in one native pass when available.
21
26
  def self.filter_records(records, required_flags: 0, excluded_flags: 0,
22
27
  min_mapq: 0, tid: nil, beg: nil, end_: nil)
@@ -38,11 +43,11 @@ module HTS
38
43
  return file unless block_given?
39
44
 
40
45
  begin
41
- yield file
46
+ result = yield file
42
47
  ensure
43
48
  file.close
44
49
  end
45
- file
50
+ result
46
51
  end
47
52
 
48
53
  def self.build_index(file_name, index_name = nil, min_shift = 0, threads = 0, verbose = true)
@@ -98,7 +103,11 @@ module HTS
98
103
 
99
104
  set_threads(threads) if threads
100
105
 
101
- return if @mode[0] == "w"
106
+ if writing?
107
+ @auto_index_on_close = build_index
108
+ @index_name_on_close = index
109
+ return
110
+ end
102
111
 
103
112
  @header = Bam::Header.new(@native.read_header)
104
113
  if build_index
@@ -120,6 +129,12 @@ module HTS
120
129
  end
121
130
 
122
131
  def load_index(index_name = nil)
132
+ return self if try_load_index(index_name)
133
+
134
+ raise MissingIndexError, "Failed to load index #{index_name || "for #{@file_name}"}"
135
+ end
136
+
137
+ def try_load_index(index_name = nil)
123
138
  check_closed
124
139
 
125
140
  @index_name = index_name
@@ -134,7 +149,16 @@ module HTS
134
149
  end
135
150
 
136
151
  def close
137
- @native&.close
152
+ was_closed = closed?
153
+ result = @native&.close
154
+ raise WriteError, "Failed to close #{@file_name}: buffered output may be incomplete" if writing? && result&.negative?
155
+
156
+ if writing? && @auto_index_on_close && !was_closed
157
+ @auto_index_on_close = false
158
+ self.class.build_index(@file_name, @index_name_on_close, 0, @nthreads || 0, false)
159
+ end
160
+
161
+ nil
138
162
  end
139
163
 
140
164
  def closed? = @native.nil? || @native.closed?
@@ -168,6 +192,8 @@ module HTS
168
192
 
169
193
  private
170
194
 
195
+ def writing? = @mode&.start_with?("w", "a")
196
+
171
197
  def native_handle = @native
172
198
 
173
199
  public
@@ -177,6 +203,7 @@ module HTS
177
203
 
178
204
  @header = header.dup
179
205
  @native.write_header(header.__send__(:native_handle))
206
+ nil
180
207
  end
181
208
 
182
209
  def header=(header)
@@ -188,10 +215,13 @@ module HTS
188
215
 
189
216
  r = @native.write(header.__send__(:native_handle), record.__send__(:native_handle))
190
217
  raise "Failed to write record" if r < 0
218
+
219
+ nil
191
220
  end
192
221
 
193
222
  def <<(record)
194
223
  write(record)
224
+ self
195
225
  end
196
226
 
197
227
  # @!macro [attach] define_getter
@@ -230,6 +260,13 @@ module HTS
230
260
  each(copy: true).to_a
231
261
  end
232
262
 
263
+ # Materialize independent, owning records. Enumerable#to_a is unsafe for
264
+ # the default reused-record iterator because every array element would
265
+ # otherwise refer to the same native bam1_t buffer.
266
+ def to_a
267
+ collect_records
268
+ end
269
+
233
270
  # @!macro [attach] define_iterator
234
271
  # @method each_$1
235
272
  # Get $1 iterator
@@ -378,7 +415,13 @@ module HTS
378
415
  return to_enum(__method__) unless block_given?
379
416
 
380
417
  record = Record.new(header)
381
- yield record while @native.read(header.__send__(:native_handle), record.__send__(:native_handle)) != -1
418
+ loop do
419
+ result = @native.read(header.__send__(:native_handle), record.__send__(:native_handle))
420
+ break if result == -1
421
+ raise ReadError, "Failed to read BAM/SAM record (HTSlib error #{result})" if result < -1
422
+
423
+ yield record
424
+ end
382
425
  self
383
426
  end
384
427
 
@@ -388,7 +431,13 @@ module HTS
388
431
  return to_enum(__method__) unless block_given?
389
432
 
390
433
  record = Record.new(header)
391
- yield record.dup while @native.read(header.__send__(:native_handle), record.__send__(:native_handle)) != -1
434
+ loop do
435
+ result = @native.read(header.__send__(:native_handle), record.__send__(:native_handle))
436
+ break if result == -1
437
+ raise ReadError, "Failed to read BAM/SAM record (HTSlib error #{result})" if result < -1
438
+
439
+ yield record.dup
440
+ end
392
441
  self
393
442
  end
394
443
 
@@ -5,14 +5,17 @@ module HTS
5
5
  class Error < HTS::Error; end
6
6
 
7
7
  class OpenError < Error; end
8
+ class WriteError < Error; end
8
9
  class IndexError < Error; end
9
10
  class MissingIndexError < IndexError; end
10
11
  class QueryError < Error; end
12
+ class RecordError < Error; end
11
13
  class HeaderError < Error; end
12
14
  class SubsetError < HeaderError; end
13
15
  class UnknownSampleError < SubsetError; end
14
16
  class FieldError < Error; end
15
17
  class InfoError < FieldError; end
18
+ class InfoDefinitionError < InfoError; end
16
19
  class InfoTypeError < InfoError; end
17
20
  class InfoReadError < InfoError; end
18
21
  class InfoUpdateError < InfoError; end
@@ -141,7 +141,14 @@ module HTS
141
141
  native.format_get(header_native, key, code, raw_float)
142
142
  end
143
143
 
144
- def get_int(key) = get(key, :int)
144
+ def get_int_raw(key) = get_raw(key, :int)
145
+
146
+ def get_int(key)
147
+ values = get_int_raw(key)
148
+ values&.each_with_object([]) do |value, decoded|
149
+ decoded << missing_int(value) unless value == Native::BCF_INT32_VECTOR_END
150
+ end
151
+ end
145
152
 
146
153
  def get_float(key)
147
154
  words = get_raw(key, :float)
@@ -44,6 +44,7 @@ module HTS
44
44
  def get_version
45
45
  @native.version
46
46
  end
47
+ alias version get_version
47
48
 
48
49
  def set_version(version)
49
50
  rc = @native.set_version(version)
@@ -53,6 +54,7 @@ module HTS
53
54
  sync_if_needed!
54
55
  self
55
56
  end
57
+ alias version= set_version
56
58
 
57
59
  def nsamples
58
60
  @native.nsamples
data/lib/hts/bcf/info.rb CHANGED
@@ -15,7 +15,7 @@ module HTS
15
15
 
16
16
  def get(key, type = nil)
17
17
  schema = header.schema("INFO", key.to_s)
18
- return nil unless schema
18
+ raise InfoDefinitionError, "INFO tag #{key} not defined in header" unless schema
19
19
 
20
20
  actual_type = schema.first
21
21
  requested = type&.to_sym
@@ -24,7 +24,8 @@ module HTS
24
24
  end
25
25
 
26
26
  code = TYPE_CODES.fetch(requested || actual_type)
27
- native.info_get(header_native, key.to_s, code)
27
+ value = native.info_get(header_native, key.to_s, code)
28
+ code == Native::BCF_HT_FLAG && value.nil? ? false : value
28
29
  end
29
30
 
30
31
  def get_int(key) = get(key, :int)
@@ -27,11 +27,12 @@ module HTS
27
27
  def id = @native.id
28
28
 
29
29
  def id=(value)
30
- @native.set_id(@header.__send__(:native_handle), value)
30
+ check_update_rc!(@native.set_id(@header.__send__(:native_handle), value), "ID", value)
31
+ value
31
32
  end
32
33
 
33
34
  def clear_id
34
- @native.set_id(@header.__send__(:native_handle), ".")
35
+ check_update_rc!(@native.set_id(@header.__send__(:native_handle), "."), "ID", ".")
35
36
  nil
36
37
  end
37
38
 
@@ -44,7 +45,8 @@ module HTS
44
45
  values = Array(values).map(&:to_s)
45
46
  raise ArgumentError, "at least one allele is required" if values.empty?
46
47
 
47
- @native.set_alleles(@header.__send__(:native_handle), values.join(","))
48
+ encoded = values.join(",")
49
+ check_update_rc!(@native.set_alleles(@header.__send__(:native_handle), encoded), "alleles", encoded)
48
50
  values
49
51
  end
50
52
 
@@ -62,15 +64,20 @@ module HTS
62
64
  @native.core_set(:qual, value)
63
65
  end
64
66
 
65
- def filter
67
+ def filters
66
68
  names = @native.filter_names(@header.__send__(:native_handle))
67
- case names.length
68
- when 0 then "PASS"
69
- when 1 then names.first
70
- else names
71
- end
69
+ names
72
70
  end
73
71
 
72
+
73
+ # VCF FILTER is a list even when it contains a single value. Keeping a
74
+ # stable return type avoids substring matching for single-filter records.
75
+ alias filter filters
76
+
77
+ def passed? = filters == ["PASS"]
78
+ def filter_missing? = filters.empty?
79
+ def filtered? = !passed? && !filter_missing?
80
+
74
81
  def each_filter_id(&block)
75
82
  return to_enum(__method__) unless block_given?
76
83
 
@@ -95,6 +102,12 @@ module HTS
95
102
 
96
103
  private
97
104
 
105
+ def check_update_rc!(result, field, value)
106
+ return result unless result.negative?
107
+
108
+ raise RecordError, "Failed to update #{field} to #{value.inspect} (HTSlib error #{result})"
109
+ end
110
+
98
111
  def native_handle = @native
99
112
 
100
113
  def initialize_copy(original)
data/lib/hts/bcf.rb CHANGED
@@ -29,11 +29,11 @@ module HTS
29
29
  return file unless block_given?
30
30
 
31
31
  begin
32
- yield file
32
+ result = yield file
33
33
  ensure
34
34
  file.close
35
35
  end
36
- file
36
+ result
37
37
  end
38
38
 
39
39
  def self.build_index(file_name, index_name = nil, min_shift = 14, threads = 0, verbose = true)
@@ -67,7 +67,11 @@ module HTS
67
67
  raise SubsetError,
68
68
  "Sample subsetting is only available when reading BCF/VCF files"
69
69
  end
70
- return if mode.start_with?("w")
70
+ if writing?
71
+ @auto_index_on_close = build_index
72
+ @index_name_on_close = index
73
+ return
74
+ end
71
75
 
72
76
  @read_header = Header.new(@native.read_header)
73
77
  @header = subset ? @read_header.subset(subset) : @read_header
@@ -91,6 +95,12 @@ module HTS
91
95
  end
92
96
 
93
97
  def load_index(index_name = nil)
98
+ return self if try_load_index(index_name)
99
+
100
+ raise MissingIndexError, "Failed to load index #{index_name || "for #{@file_name}"}"
101
+ end
102
+
103
+ def try_load_index(index_name = nil)
94
104
  check_closed
95
105
  @index_name = index_name
96
106
  @index_load_attempted = true
@@ -102,7 +112,18 @@ module HTS
102
112
  @native.index_loaded?
103
113
  end
104
114
 
105
- def close = @native&.close
115
+ def close
116
+ was_closed = closed?
117
+ result = @native&.close
118
+ raise WriteError, "Failed to close #{@file_name}: buffered output may be incomplete" if writing? && result&.negative?
119
+
120
+ if writing? && @auto_index_on_close && !was_closed
121
+ @auto_index_on_close = false
122
+ self.class.build_index(@file_name, @index_name_on_close, 14, @nthreads || 0, false)
123
+ end
124
+
125
+ nil
126
+ end
106
127
  def closed? = @native.nil? || @native.closed?
107
128
  def file_format = @native.file_format
108
129
  def file_format_version = @native.file_format_version
@@ -137,7 +158,7 @@ module HTS
137
158
  result = @native.write_header(header.__send__(:native_handle))
138
159
  raise HeaderError, "Failed to write BCF header" if result.negative?
139
160
 
140
- result
161
+ nil
141
162
  end
142
163
 
143
164
  def header=(header)
@@ -149,9 +170,13 @@ module HTS
149
170
  result = @native.write(header.__send__(:native_handle), record.__send__(:native_handle))
150
171
  raise "Failed to write record" if result.negative?
151
172
 
152
- result
173
+ nil
174
+ end
175
+
176
+ def <<(record)
177
+ write(record)
178
+ self
153
179
  end
154
- alias << write
155
180
 
156
181
  def nsamples
157
182
  check_closed
@@ -192,6 +217,10 @@ module HTS
192
217
 
193
218
  def collect_records = each(copy: true).to_a
194
219
 
220
+ # Materialize independent, owning records rather than retaining the
221
+ # reused Record yielded by the allocation-conscious default iterator.
222
+ def to_a = collect_records
223
+
195
224
  define_iterator :chrom
196
225
  define_iterator :pos
197
226
  define_iterator :endpos
@@ -244,6 +273,8 @@ module HTS
244
273
 
245
274
  private
246
275
 
276
+ def writing? = @mode&.start_with?("w", "a")
277
+
247
278
  def ensure_index_loaded
248
279
  return true if index_loaded?
249
280
  return false if @index_load_attempted
data/lib/hts/faidx.rb CHANGED
@@ -4,6 +4,8 @@ require_relative "native"
4
4
 
5
5
  module HTS
6
6
  class Faidx
7
+ class OpenError < HTS::Error; end
8
+
7
9
  attr_reader :file_name, :format
8
10
 
9
11
  def self.open(file_name, format: :auto, auto_build: true)
data/lib/hts/tabix.rb CHANGED
@@ -9,6 +9,9 @@ module HTS
9
9
  class Tabix < Hts
10
10
  include Enumerable
11
11
 
12
+ class OpenError < HTS::Error; end
13
+ class MissingIndexError < HTS::Error; end
14
+
12
15
  attr_reader :file_name, :index_name, :mode, :nthreads
13
16
 
14
17
  def self.open(*args, **kw)
@@ -16,11 +19,11 @@ module HTS
16
19
  return file unless block_given?
17
20
 
18
21
  begin
19
- yield file
22
+ result = yield file
20
23
  ensure
21
24
  file.close
22
25
  end
23
- file
26
+ result
24
27
  end
25
28
 
26
29
  def initialize(file_name, index: nil, threads: nil, build_index: false)
@@ -40,8 +43,12 @@ module HTS
40
43
 
41
44
  set_threads(threads) if threads
42
45
 
43
- # build_index(index) if build_index
44
- load_index(index) if index
46
+ if build_index
47
+ build_index(index)
48
+ load_index(index)
49
+ elsif index
50
+ load_index(index)
51
+ end
45
52
  end
46
53
 
47
54
  def build_index(index_name = nil, min_shift: 0)
@@ -70,6 +77,12 @@ module HTS
70
77
  end
71
78
 
72
79
  def load_index(index_name = nil)
80
+ return self if try_load_index(index_name)
81
+
82
+ raise MissingIndexError, "Failed to load index #{index_name || "for #{@file_name}"}"
83
+ end
84
+
85
+ def try_load_index(index_name = nil)
73
86
  check_closed
74
87
  @index_name = index_name
75
88
  @index_load_attempted = true
data/lib/hts/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTS
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: htslib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2