@gmod/tabix 3.4.2 → 3.5.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.
package/src/tbi.ts CHANGED
@@ -17,21 +17,31 @@ import type VirtualOffset from './virtualOffset.ts'
17
17
 
18
18
  const TBI_MAGIC = 21_578_324 // TBI\1
19
19
 
20
+ // TBI is always the fixed depth-5, 14-bit-leaf binning scheme
21
+ const TBI_DEPTH = 5
22
+ const TBI_MAX_REF_LENGTH = 2 ** (14 + TBI_DEPTH * 3)
23
+
20
24
  /**
21
25
  * calculate the list of bins that may overlap with region [beg,end)
22
26
  * (zero-based half-open)
23
27
  */
24
28
  function reg2bins(beg: number, end: number) {
25
- beg += 1 // < convert to 1-based closed
26
- end -= 1
27
- return [
28
- [0, 0],
29
- [1 + (beg >> 26), 1 + (end >> 26)],
30
- [9 + (beg >> 23), 9 + (end >> 23)],
31
- [73 + (beg >> 20), 73 + (end >> 20)],
32
- [585 + (beg >> 17), 585 + (end >> 17)],
33
- [4681 + (beg >> 14), 4681 + (end >> 14)],
34
- ] as const
29
+ // Clamp into the binning scheme's range. Past 2**31 the shifts below
30
+ // truncate to int32 and go negative, which would silently yield no bins.
31
+ const b = Math.min(Math.max(beg, 0), TBI_MAX_REF_LENGTH)
32
+ const e = Math.min(end, TBI_MAX_REF_LENGTH) - 1
33
+ const bins: (readonly [number, number])[] = []
34
+ if (b <= e) {
35
+ bins.push(
36
+ [0, 0],
37
+ [1 + (b >> 26), 1 + (e >> 26)],
38
+ [9 + (b >> 23), 9 + (e >> 23)],
39
+ [73 + (b >> 20), 73 + (e >> 20)],
40
+ [585 + (b >> 17), 585 + (e >> 17)],
41
+ [4681 + (b >> 14), 4681 + (e >> 14)],
42
+ )
43
+ }
44
+ return bins
35
45
  }
36
46
 
37
47
  export default class TabixIndex extends IndexFile {
@@ -42,16 +52,18 @@ export default class TabixIndex extends IndexFile {
42
52
  onProgress: opts.onProgress,
43
53
  })
44
54
  const bytes = (await unzip(buf)) as Uint8Array
45
- const dataView = new DataView(bytes.buffer)
55
+ const dataView = new DataView(
56
+ bytes.buffer,
57
+ bytes.byteOffset,
58
+ bytes.byteLength,
59
+ )
46
60
 
47
61
  if (dataView.getUint32(0, true) !== TBI_MAGIC) {
48
62
  throw new Error('Not a TBI file')
49
63
  }
50
64
 
51
65
  const refCount = dataView.getUint32(4, true)
52
- const depth = 5
53
- const maxBinNumber = ((1 << ((depth + 1) * 3)) - 1) / 7
54
- const maxRefLength = 2 ** (14 + depth * 3)
66
+ const maxBinNumber = ((1 << ((TBI_DEPTH + 1) * 3)) - 1) / 7
55
67
 
56
68
  // TBI header layout matches CSI aux data; parseAuxData handles both
57
69
  const {
@@ -159,7 +171,7 @@ export default class TabixIndex extends IndexFile {
159
171
  indices: memoizeByRefId(getIndices),
160
172
  metaChar,
161
173
  maxBinNumber,
162
- maxRefLength,
174
+ maxRefLength: TBI_MAX_REF_LENGTH,
163
175
  skipLines,
164
176
  firstDataLine,
165
177
  columnNumbers,
@@ -177,10 +189,6 @@ export default class TabixIndex extends IndexFile {
177
189
  max: number,
178
190
  opts: Options = {},
179
191
  ) {
180
- if (min < 0) {
181
- min = 0
182
- }
183
-
184
192
  const indexData = await this.parse(opts)
185
193
  const refId = indexData.refNameToId[refName]
186
194
  if (refId === undefined) {
@@ -191,13 +199,12 @@ export default class TabixIndex extends IndexFile {
191
199
  return []
192
200
  }
193
201
 
194
- const linearIndex = ba.linearIndex ?? []
195
- // min >= 2**31 overflows int32 and produces a negative >> 14 result —
196
- // query is well beyond the indexed range
197
- if (linearIndex.length > 0 && min >= 2 ** 31) {
202
+ if (min > TBI_MAX_REF_LENGTH) {
198
203
  console.warn('querying outside of possible tabix range')
199
204
  }
200
-
205
+ // clamp before the >> 14 below, which would truncate to int32 and go
206
+ // negative past 2**31
207
+ const beg = Math.min(Math.max(min, 0), TBI_MAX_REF_LENGTH)
201
208
  const overlappingBins = reg2bins(min, max) // List of bin #s that overlap min, max
202
209
  const chunks: Chunk[] = []
203
210
 
@@ -216,7 +223,8 @@ export default class TabixIndex extends IndexFile {
216
223
  // The linear index is monotonically non-decreasing, so the minimum virtual
217
224
  // offset for chunks that could overlap [min, ...) is at index minLin.
218
225
  // SYNC: ~/src/gmod/bam-js/src/bai.ts getLowestChunk
219
- const lowest = linearIndex[Math.min(min >> 14, linearIndex.length - 1)]
226
+ const linearIndex = ba.linearIndex
227
+ const lowest = linearIndex?.[Math.min(beg >> 14, linearIndex.length - 1)]
220
228
 
221
229
  return optimizeChunks(chunks, lowest)
222
230
  }
package/src/util.ts CHANGED
@@ -86,7 +86,13 @@ export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
86
86
  // maxv.blockPosition is an upper bound on where that final block ends — always at
87
87
  // least the true block end, so the clamped fetch still contains the whole block.
88
88
  // Shrinks both the byte estimate and the actual fetch with no extra I/O.
89
- export function clampChunkEnds(chunks: Chunk[], extraBoundaries: number[] = []) {
89
+ export function clampChunkEnds(
90
+ chunks: Chunk[],
91
+ extraBoundaries: number[] = [],
92
+ ) {
93
+ // Array#sort with a comparator beats a Float64Array numeric sort here:
94
+ // extraBoundaries (the linear index) arrives already sorted and TimSort
95
+ // exploits the existing run, which introsort cannot.
90
96
  const boundaries = [...extraBoundaries]
91
97
  for (const c of chunks) {
92
98
  boundaries.push(c.minv.blockPosition, c.maxv.blockPosition)
@@ -150,7 +156,11 @@ const tabixFormats: Record<number, string> = {
150
156
  }
151
157
 
152
158
  export function parseAuxData(bytes: Uint8Array, offset: number) {
153
- const dataView = new DataView(bytes.buffer)
159
+ const dataView = new DataView(
160
+ bytes.buffer,
161
+ bytes.byteOffset,
162
+ bytes.byteLength,
163
+ )
154
164
  const formatFlags = dataView.getInt32(offset, true)
155
165
  const coordinateType =
156
166
  formatFlags & 0x1_00_00 ? 'zero-based-half-open' : '1-based-closed'