Disclaimer: I used IA to help me make this bug report understandable
libmseed version: 3.5.4 (also verified on git master v3.5.4-12-g...)
Environment: Linux x86_64, gcc 12.2, glibc 2.36
Summary
ms3_readtracelist_selection returns zero matching trace segments when called with both a byte-range end offset (via MSF_PNAMERANGE) and a time-window selection, even though the byte range contains records whose data overlaps the requested time window. Removing either the end offset or the time selection yields the correct result.
Test file
https://s3.seisdata.epos-france.fr/mseed/FR/2014/MLS/HHN.D/FR.MLS.00.HHN.D.2014.046
Time index (from mseedindex):
| File offset |
Record start time (UTC) |
Approx clock time |
| 196608 |
2014-046T05:17:09.616900Z |
05:17:09 |
| 274432 |
2014-046T05:59:46.836900Z |
05:59:46 |
| 471040 |
2014-046T06:00:17.497000Z |
06:00:17 |
| 1646592 |
2014-046T13:19:48.196200Z |
13:19:48 |
Channel is 100 Hz. Between offsets 196608 and 274432 the file is not a single record — there are many miniSEED records packed contiguously within that byte range (the mseedindex entries are merely the first record at each indexed offset). The record boundaries can be enumerated by passing MSF_RECORDLIST.
Query window: 2014-046 05:18:30 – 05:19:00 (30 s, 3000 samples at 100 Hz).
This window falls entirely inside the byte range [196608, 274432), so MSF_PNAMERANGE should not truncate any needed data.
Reproducer (C, self-contained)
The code below downloads the test file once via curl (requires network), then runs four scenarios against the same local copy.
/*
* gcc -o repro repro.c -I/path/to/libmseed -L/path/to/libmseed -lmseed
* LD_LIBRARY_PATH=/path/to/libmseed ./repro
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "libmseed.h"
#define TEST_URL "https://s3.seisdata.epos-france.fr/mseed/FR/2014/MLS/HHN.D/FR.MLS.00.HHN.D.2014.046"
#define TEST_FILE "/tmp/libmseed_bug_test.mseed"
static int fetch_file(void) {
FILE *fp = fopen(TEST_FILE, "rb");
if (fp) { fclose(fp); return 0; }
char cmd[512];
snprintf(cmd, sizeof(cmd), "curl -s -o %s %s", TEST_FILE, TEST_URL);
int rc = system(cmd);
if (rc != 0) {
fprintf(stderr, "Please download manually:\n wget -O %s %s\n",
TEST_FILE, TEST_URL);
}
return rc;
}
/* Return the number of trace segments found by ms3_readtracelist_selection,
* or -1 on error. rc_out receives the raw return code from the library. */
static int count_segments(const char *filepath,
int64_t t_start, int64_t t_end,
int *rc_out) {
MS3TraceList *mstl = NULL;
MS3Selections *sel = NULL;
int nseg = 0;
if (t_start || t_end)
ms3_addselect(&sel, "FDSN:FR_MLS_00_H_H_N", t_start, t_end, 0);
int rc = ms3_readtracelist_selection(
&mstl, filepath, NULL, sel, 0,
MSF_PNAMERANGE | MSF_RECORDLIST, 0);
if (rc_out) *rc_out = rc;
if (rc == MS_NOERROR && mstl && mstl->numtraceids > 0)
nseg = mstl->traces.next[0]->numsegments;
mstl3_free(&mstl, 1);
ms3_freeselections(sel);
return nseg;
}
int main(void) {
if (fetch_file() != 0) return 1;
/* Query window */
int64_t t_lo = ms_timestr2hptime("2014,046,05,18,30.000000");
int64_t t_hi = ms_timestr2hptime("2014,046,05,19,00.000000");
int rc;
/* ---- Test 1: baseline — no time filter, no end offset ---- */
int s1 = count_segments(TEST_FILE "@196608", 0, 0, &rc);
/* rc here is the raw return value from ms3_readtracelist_selection.
* MS_NOERROR (0) = success.
* MS_ENDOFFILE(-1)= no more data (not an error).
* Other values = actual errors. */
/* ---- Test 2: no time filter, WITH end offset ---- */
int s2 = count_segments(TEST_FILE "@196608:274432", 0, 0, &rc);
/* ---- Test 3: WITH time filter, no end offset (CONTROL) ---- */
int s3 = count_segments(TEST_FILE "@196608", t_lo, t_hi, &rc);
/* ---- Test 4: WITH time filter, WITH end offset (BUG TRIGGER) ---- */
int s4 = count_segments(TEST_FILE "@196608:274432", t_lo, t_hi, &rc);
printf("Test segments\n");
printf("────────────────────────────────────────────────\n");
printf("1. No time, no end offset %d\n", s1);
printf("2. No time, with end offset %d\n", s2);
printf("3. WITH time, no end offset (ctrl) %d\n", s3);
printf("4. WITH time, with end offset (BUG) %d ← SHOULD BE >0\n", s4);
/* Additional probes — wider end offsets still trigger the bug */
int s5 = count_segments(TEST_FILE "@196608:1646592", t_lo, t_hi, &rc);
int s6 = count_segments(TEST_FILE "@196608:4816896", t_lo, t_hi, &rc);
printf("5. end=1646592 + time %d\n", s5);
printf("6. end=EOF + time %d\n", s6);
remove(TEST_FILE);
return (s3 > 0 && s4 == 0) ? 1 : 0;
}
Expected output
Test segments
────────────────────────────────────────────────
1. No time, no end offset 1
2. No time, with end offset 1
3. WITH time, no end offset (ctrl) 1
4. WITH time, with end offset (BUG) 1 ← SHOULD BE >0
5. end=1646592 + time 1
6. end=EOF + time 1
Actual output (bug)
Test segments
────────────────────────────────────────────────
1. No time, no end offset 1
2. No time, with end offset 1
3. WITH time, no end offset (ctrl) 1
4. WITH time, with end offset (BUG) 0 ← SHOULD BE >0
5. end=1646592 + time 0
6. end=EOF + time 1
Observations:
- Tests 4 and 5 return zero segments even though data exists within the time window and falls inside the byte range.
- Test 6 works (
end=EOF), proving the time selection itself is correct — the bug only manifests when a finite end_offset is combined with a time window.
dataselect commands (same bug, external tool)
The dataselect binary (EarthScope/dataselect) uses the same ms3_readtracelist_selection code path:
### Download once
wget https://s3.seisdata.epos-france.fr/mseed/FR/2014/MLS/HHN.D/FR.MLS.00.HHN.D.2014.046
### [A] end offset, NO time filter → 2 segments (OK)
dataselect -Ps +o - FR.MLS.00.HHN.D.2014.046@196608:274432 | msi -tg -
### [B] end offset, WITH time filter → "No SEED data detected" (BUG)
dataselect -Ps \
-ts 2014,046,05,18,30.000000 -te 2014,046,05,19,00.000000 \
+o - FR.MLS.00.HHN.D.2014.046@196608:274432 | msi -tg -
### [C] SAME time filter, NO end offset → 1 segment, 3000 samples (OK)
dataselect -Ps \
-ts 2014,046,05,18,30.000000 -te 2014,046,05,19,00.000000 \
+o - FR.MLS.00.HHN.D.2014.046@196608 | msi -tg -
[B] is broken; [A] and [C] work.
Root-cause hypothesis
The issue lies in the interaction between MSF_PNAMERANGE and time-based filtering inside ms3_readtracelist_selection. I traced the logic as follows (libmseed 3.5.4 source in selection.c and tracelist.c):
-
Pass 1 — header scan: The function reads record headers sequentially, starting at start_offset, using ms3_readmsr. With MSF_PNAMERANGE it stops when the file position reaches end_offset.
-
Time matching: For each header, ms3_matchselection checks rec->starttime <= sel_end && rec->endtime >= sel_start. The record's endtime is computed as starttime + (samplecnt - 1) / samprate.
-
The bug trigger: When a finite end_offset is set and the record starting at start_offset has a starttime before the query window's starttime, the trace list builder inside ms3_readtracelist_selection appears to use a different code path that skips the record entirely instead of trimming it to the overlapping portion. This code path is apparently taken only when both conditions hold — neither condition alone triggers it.
Specifically, in selection.c:ms_readtraceselection() the PNAMERANGE branch may terminate the record loop prematurely when the last header read before end_offset has a starttime < sel_start, even if that same record's data extends into the window.
-
Why test 6 works: With end_offset = EOF (no end bound) the PNAMERANGE branch is never taken; the reader follows the normal path which correctly handles overlapping records.
A fix would likely involve modifying the PNAMERANGE branch in ms_readtraceselection() (or the equivalent logic in ms3_readtracelist_selection()) to not discard a record until its endtime has been verified against the selection window, rather than checking only starttime.
Workaround
For callers that control the byte range: always set end_offset to at least the byte position of the next record boundary after the window of interest, or use end_offset = 0 (file size) when feasible. In the dataselect case, omitting the :end suffix from the path avoids the bug entirely.
libmseed version: 3.5.4 (also verified on git master
v3.5.4-12-g...)Environment: Linux x86_64, gcc 12.2, glibc 2.36
Summary
ms3_readtracelist_selectionreturns zero matching trace segments when called with both a byte-range end offset (viaMSF_PNAMERANGE) and a time-window selection, even though the byte range contains records whose data overlaps the requested time window. Removing either the end offset or the time selection yields the correct result.Test file
Time index (from
mseedindex):Channel is 100 Hz. Between offsets 196608 and 274432 the file is not a single record — there are many miniSEED records packed contiguously within that byte range (the
mseedindexentries are merely the first record at each indexed offset). The record boundaries can be enumerated by passingMSF_RECORDLIST.Query window:
2014-046 05:18:30 – 05:19:00(30 s, 3000 samples at 100 Hz).This window falls entirely inside the byte range
[196608, 274432), soMSF_PNAMERANGEshould not truncate any needed data.Reproducer (C, self-contained)
The code below downloads the test file once via
curl(requires network), then runs four scenarios against the same local copy.Expected output
Actual output (bug)
Observations:
end=EOF), proving the time selection itself is correct — the bug only manifests when a finiteend_offsetis combined with a time window.dataselectcommands (same bug, external tool)The
dataselectbinary (EarthScope/dataselect) uses the samems3_readtracelist_selectioncode path:[B]is broken;[A]and[C]work.Root-cause hypothesis
The issue lies in the interaction between
MSF_PNAMERANGEand time-based filtering insidems3_readtracelist_selection. I traced the logic as follows (libmseed 3.5.4 source inselection.candtracelist.c):Pass 1 — header scan: The function reads record headers sequentially, starting at
start_offset, usingms3_readmsr. WithMSF_PNAMERANGEit stops when the file position reachesend_offset.Time matching: For each header,
ms3_matchselectionchecksrec->starttime <= sel_end && rec->endtime >= sel_start. The record'sendtimeis computed asstarttime + (samplecnt - 1) / samprate.The bug trigger: When a finite
end_offsetis set and the record starting atstart_offsethas astarttimebefore the query window'sstarttime, the trace list builder insidems3_readtracelist_selectionappears to use a different code path that skips the record entirely instead of trimming it to the overlapping portion. This code path is apparently taken only when both conditions hold — neither condition alone triggers it.Specifically, in
selection.c:ms_readtraceselection()thePNAMERANGEbranch may terminate the record loop prematurely when the last header read beforeend_offsethas astarttime < sel_start, even if that same record's data extends into the window.Why test 6 works: With
end_offset = EOF(no end bound) thePNAMERANGEbranch is never taken; the reader follows the normal path which correctly handles overlapping records.A fix would likely involve modifying the
PNAMERANGEbranch inms_readtraceselection()(or the equivalent logic inms3_readtracelist_selection()) to not discard a record until itsendtimehas been verified against the selection window, rather than checking onlystarttime.Workaround
For callers that control the byte range: always set
end_offsetto at least the byte position of the next record boundary after the window of interest, or useend_offset = 0(file size) when feasible. In the dataselect case, omitting the:endsuffix from the path avoids the bug entirely.