6c676a0e9ed642676008731762141e0c16822787 braney Sat Aug 22 09:31:14 2026 -0700 vcf: tighten validation of the genotype allele index, refs #38155 New parseAlleleIx returns missing data for a GT allele index that the record has no allele for, and for a value that will not fit the field. It checks the value as an int, before it reaches the field. vcfParseGenotypes and vcfParseGenotypesGtOnly both go through it, and a check after the per-genotype loop covers the PL and SGT fallbacks, which choose an index without consulting the allele count. hapIxA and hapIxB become signed char. Plain char is unsigned on some architectures, and the negative missing-data value did not stay negative there. Size and field offsets are unchanged, and x86 code generation is identical. Adds a lib/tests case for malformed and boundary genotype indexes. diff --git src/lib/tests/vcfParseTest.c src/lib/tests/vcfParseTest.c index 0050e16f9d0..9ba0f25959e 100644 --- src/lib/tests/vcfParseTest.c +++ src/lib/tests/vcfParseTest.c @@ -8,60 +8,96 @@ #include "options.h" #include "sqlNum.h" #include "vcf.h" void usage() /* Explain usage and exit. */ { errAbort( "vcfParseTest - Parse VCF header and data lines in given position range.\n" "usage:\n" " vcfParseTest fileOrUrl.vcf.gz seqName start end\n" "options:\n" " -headerOnly print header summary only (version, def counts, sample IDs)\n" " and skip the position-range parse. seqName/start/end are ignored.\n" + " -genotypes print the parsed allele index of each genotype, and the allele\n" + " counts tallied from them.\n" "\n" "fileOrUrl.vcf.gz needs to have been compressed by tabix, and index file\n" "fileOrUrl.vcf.gz.tbi must exist.\n" ); } static struct optionSpec options[] = { {"headerOnly", OPTION_BOOLEAN}, + {"genotypes", OPTION_BOOLEAN}, {NULL, 0}, }; static void vcfHeaderTest(char *fileOrUrl) /* Open via tabix path and dump header summary -- regression-tests that the * VCF header is actually read through the tabix iterator code path. */ { struct vcfFile *vcff = vcfTabixFileMayOpen(fileOrUrl, NULL, 0, 0, 100, -1); if (vcff == NULL) errAbort("Failed to open \"%s\"", fileOrUrl); printf("file: %s\n", fileOrUrl); printf("version: %d.%d\n", vcff->majorVersion, vcff->minorVersion); printf("infoDefs: %d\n", slCount(vcff->infoDefs)); printf("filterDefs: %d\n", slCount(vcff->filterDefs)); printf("gtFormatDefs: %d\n", slCount(vcff->gtFormatDefs)); printf("genotypeCount: %d\n", vcff->genotypeCount); int i; for (i = 0; i < vcff->genotypeCount && i < 5; i++) printf("genotypeId[%d]: %s\n", i, vcff->genotypeIds[i]); vcfFileFree(&vcff); } +static void vcfGenotypeTest(char *fileOrUrl, char *seqName, int start, int end) +/* Dump the allele index that each genotype parsed to, and the allele counts tallied from + * those indexes. An index that the record has no allele for should come out as missing + * data (-1). */ +{ +struct vcfFile *vcff = vcfTabixFileMayOpen(fileOrUrl, seqName, start, end, 100, -1); +if (vcff == NULL) + errAbort("Failed to parse \"%s\" and/or its index file \"%s.tbi\"", fileOrUrl, fileOrUrl); +struct vcfRecord *rec; +for (rec = vcff->records; rec != NULL; rec = rec->next) + { + vcfParseGenotypes(rec); + printf("%s\t%d\t%s\talleleCount=%d\n", rec->chrom, rec->chromStart, rec->name, + rec->alleleCount); + int i; + for (i = 0; i < vcff->genotypeCount; i++) + { + struct vcfGenotype *gt = &(rec->genotypes[i]); + printf(" %s\thapIxA=%d\thapIxB=%d\tisHaploid=%d\tisPhased=%d\n", + vcff->genotypeIds[i], gt->hapIxA, gt->hapIxB, gt->isHaploid, gt->isPhased); + } + int *gtCounts = NULL, *alCounts = NULL, phasedCount = 0, diploidCount = 0; + vcfCountGenotypes(rec, >Counts, &alCounts, &phasedCount, &diploidCount); + printf(" alleleCounts (last is missing data):"); + for (i = 0; i <= rec->alleleCount; i++) + printf(" %d", alCounts[i]); + printf("\n phased=%d diploid=%d\n", phasedCount, diploidCount); + freeMem(gtCounts); + freeMem(alCounts); + } +vcfFileFree(&vcff); +} + void vcfParseTest(char *fileOrUrl, char *seqName, int start, int end) /* vcfParseTest - Parse VCF header and data lines in given position range.. */ { struct vcfFile *vcff = vcfTabixFileMayOpen(fileOrUrl, seqName, start, end, 100, -1); if (vcff == NULL) errAbort("Failed to parse \"%s\" and/or its index file \"%s.tbi\"", fileOrUrl, fileOrUrl); int recCount = slCount(vcff->records); printf("Finished parsing \"%s\" items in %s:%d-%d, got %d data rows\n", fileOrUrl, seqName, start+1, end, recCount); if (recCount > 0) printf("First (up to) 100 rows in range:\n"); int i = 0; struct vcfRecord *rec = vcff->records; while (rec != NULL && i < 100) { @@ -75,18 +111,21 @@ } int main(int argc, char *argv[]) /* Process command line. */ { optionInit(&argc, argv, options); if (optionExists("headerOnly")) { if (argc != 2) usage(); vcfHeaderTest(argv[1]); return 0; } if (argc != 5) usage(); +if (optionExists("genotypes")) + vcfGenotypeTest(argv[1], argv[2], sqlUnsigned(argv[3]), sqlUnsigned(argv[4])); +else vcfParseTest(argv[1], argv[2], sqlUnsigned(argv[3]), sqlUnsigned(argv[4])); return 0; }