Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libmseed for openSUSE:Factory 
checked in at 2026-07-31 15:27:29
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libmseed (Old)
 and      /work/SRC/openSUSE:Factory/.libmseed.new.2004 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libmseed"

Fri Jul 31 15:27:29 2026 rev:17 rq:1368676 version:3.5.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/libmseed/libmseed.changes        2026-07-26 
11:33:08.140348491 +0200
+++ /work/SRC/openSUSE:Factory/.libmseed.new.2004/libmseed.changes      
2026-07-31 16:08:54.807781433 +0200
@@ -1,0 +2,23 @@
+Thu Jul 30 18:12:18 UTC 2026 - Andreas Stieger <[email protected]>
+
+- update to 3.5.3:
+  * optimizations for segment handling
+
+-------------------------------------------------------------------
+Wed Jul 29 20:26:15 UTC 2026 - Andreas Stieger <[email protected]>
+
+- update to 3.5.2:
+  * Add MSF_RECORDLIST_NOEXTRAS to skip copying extra headers into
+    MS3RecordList entries when building a record list.
+  * Reject a blockette offset within the fixed header as fatal.
+  * Break ms_parse_raw2() blockette walk on unknown length, treat
+    a self-referential next offset as fatal, add Blockette 405 length.
+  * Reject sample count that would overflow size_t when converting to
+    doubles in mstl3_convertsamples(), avoiding undersized malloc on
+    32-bit.
+  * Reject size_t overflow in libmseed_memory_prealloc() growth
+    loop, avoiding an infinite hang on huge allocation requests.
+  * Free duplicated MS3Record when record-list entry allocation fails.
+  * Reject out-of-range leap-second epochs in ms_readleapsecondfile().
+
+-------------------------------------------------------------------

Old:
----
  libmseed-3.5.1.tar.gz

New:
----
  libmseed-3.5.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libmseed.spec ++++++
--- /var/tmp/diff_new_pack.tJh95d/_old  2026-07-31 16:08:55.351800358 +0200
+++ /var/tmp/diff_new_pack.tJh95d/_new  2026-07-31 16:08:55.355800497 +0200
@@ -18,7 +18,7 @@
 
 %define sover 3
 Name:           libmseed
-Version:        3.5.1
+Version:        3.5.3
 Release:        0
 Summary:        MiniSEED data format library
 License:        Apache-2.0

++++++ libmseed-3.5.1.tar.gz -> libmseed-3.5.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/ChangeLog new/libmseed-3.5.3/ChangeLog
--- old/libmseed-3.5.1/ChangeLog        2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/ChangeLog        2026-07-30 18:15:35.000000000 +0200
@@ -1,4 +1,28 @@
-2026.206: v3.5.1
+2026.211: v3.5.3
+  - Optimize segment searches by tracking recently-active segments per trace 
ID,
+    a significant improvement for creating trace lists from near time ordered
+    input data.
+  - End the segment search early once a segment starts beyond the record
+    end plus tolerance, as no later segment can match in the sorted list.
+  - Search backward from the last segment for zero sample rate records,
+    which can only match a following segment, avoiding a full list scan.
+  - Avoid FP division in sample rate comparisons: short-circuit identical
+    rates and use a multiplicative form of MS_ISRATETOLERABLE().
+
+2026.208: v3.5.2
+  - Add MSF_RECORDLIST_NOEXTRAS to skip copying extra headers into
+    MS3RecordList entries when building a record list.
+  - Reject a blockette offset within the fixed header as fatal.
+  - Break ms_parse_raw2() blockette walk on unknown length, treat a
+    self-referential next offset as fatal, add Blockette 405 length.
+  - Reject sample count that would overflow size_t when converting to
+    doubles in mstl3_convertsamples(), avoiding undersized malloc on 32-bit.
+  - Reject size_t overflow in libmseed_memory_prealloc() growth loop,
+    avoiding an infinite hang on huge allocation requests.
+  - Free duplicated MS3Record when record-list entry allocation fails.
+  - Reject out-of-range leap-second epochs in ms_readleapsecondfile().
+
+  2026.206: v3.5.1
   - Set MS3RecordPtr.msr->record to NULL when the raw record is not available,
     instead of leaving it dangling to a source buffer that may be freed or
     reused.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/fileutils.c 
new/libmseed-3.5.3/fileutils.c
--- old/libmseed-3.5.1/fileutils.c      2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/fileutils.c      2026-07-30 18:15:35.000000000 +0200
@@ -790,6 +790,7 @@
  * If the ::MSF_RECORDLIST flag is set in @p flags, a ::MS3RecordList
  * will be built for each ::MS3TraceSeg.  The ::MS3RecordPtr entries
  * contain the location of the data record, bit flags, extra headers, etc.
+ * Extra headers are omitted if ::MSF_RECORDLIST_NOEXTRAS is also set.
  *
  * @param[out] ppmstl Pointer-to-pointer to a ::MS3TraceList to populate
  * @param[in] mspath File to read
@@ -799,6 +800,7 @@
  * @param[in] flags
  * @parblock
  *  - @c ::MSF_RECORDLIST : Build a ::MS3RecordList for each ::MS3TraceSeg
+ *  - @c ::MSF_RECORDLIST_NOEXTRAS : Do not copy extra headers into record 
list entries
  *  - @c ::MSF_SKIPADJACENTDUPLICATES : Skip adjacent duplicate records
  *  - Flags supported by msr3_parse()
  *  - Flags supported by mstl3_addmsr()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/genutils.c 
new/libmseed-3.5.3/genutils.c
--- old/libmseed-3.5.1/genutils.c       2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/genutils.c       2026-07-30 18:15:35.000000000 +0200
@@ -59,9 +59,16 @@
     return ptr;
 
   /* Calculate new size needed for request by adding blocks */
+  if (*currentsize > SIZE_MAX - libmseed_prealloc_block_size)
+    return NULL;
+
   newsize = *currentsize + libmseed_prealloc_block_size;
   while (newsize < size)
+  {
+    if (newsize > SIZE_MAX - libmseed_prealloc_block_size)
+      return NULL;
     newsize += libmseed_prealloc_block_size;
+  }
 
   newptr = libmseed_memory.realloc (ptr, newsize);
 
@@ -2052,6 +2059,16 @@
 
     if (fields == 2)
     {
+      /* Convert NTP epoch to Unix epoch; guard against int64 overflow */
+      leapsecond = leapsecond - NTPPOSIXEPOCHDELTA;
+
+      if (leapsecond > INT64_MAX / NSTMODULUS || leapsecond < INT64_MIN / 
NSTMODULUS)
+      {
+        ms_log (2, "Leap second epoch is beyond the representable nstime 
range\n");
+        fclose (fp);
+        return -1;
+      }
+
       if ((ls = (LeapSecond *)libmseed_memory.malloc (sizeof (LeapSecond))) == 
NULL)
       {
         ms_log (2, "Cannot allocate LeapSecond entry, out of memory?\n");
@@ -2059,8 +2076,7 @@
         return -1;
       }
 
-      /* Convert NTP epoch time to Unix epoch time and then to nttime_t */
-      ls->leapsecond = MS_EPOCH2NSTIME (leapsecond - NTPPOSIXEPOCHDELTA);
+      ls->leapsecond = MS_EPOCH2NSTIME (leapsecond);
       ls->TAIdelta = TAIdelta;
       ls->next = NULL;
       count++;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/internalstate.h 
new/libmseed-3.5.3/internalstate.h
--- old/libmseed-3.5.1/internalstate.h  2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/internalstate.h  2026-07-30 18:15:35.000000000 +0200
@@ -3,7 +3,7 @@
  *
  * This file is part of the miniSEED Library.
  *
- * Copyright (c) 2024 Chad Trabant, EarthScope Data Services
+ * Copyright (c) 2026 Chad Trabant, EarthScope Data Services
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -74,6 +74,44 @@
   int64_t totalpackedrecords;  /* Total records packed */
 };
 
+/* Number of most-recently-active segments tracked per MS3TraceID, used to
+ * bound the segment-list search in _mstl3_addmsr_impl() */
+#define LM_RECENTSEGS 4
+
+/* Maximum hops walked when resolving list order among recent segments or
+ * falling back to a direct list walk, beyond which the fast path is
+ * refused in favor of a full scan */
+#define LM_RECENTSEGS_MAXWALK 8
+
+/* Private extension of MS3TraceList (opaque in public header).
+ *
+ * The public struct is the first member so public pointers, sizeof, and
+ * field offsets are unaffected by the extension. */
+typedef struct
+{
+  MS3TraceList mstl;
+  int8_t foreignid; /* Set if an MS3TraceID not allocated by this library may 
be present */
+} LMTraceListNode;
+
+/* Private extension of MS3TraceID (opaque in public header).
+ *
+ * Tracks the most-recently-active segments of a trace ID (the "recent set")
+ * and nonrecentendbound, an upper bound on the end time of every segment
+ * NOT in the recent set.  The bound may overestimate, which only costs
+ * search performance, but must never underestimate, which would cause a
+ * match to be missed.  It starts at INT64_MIN and only ever rises, folding
+ * in the end time of segments as they are evicted from the recent set or
+ * removed from the trace list.
+ *
+ * The public struct is the first member so public pointers, sizeof, and
+ * field offsets are unaffected by the extension. */
+typedef struct
+{
+  MS3TraceID id;
+  MS3TraceSeg *recentseg[LM_RECENTSEGS];
+  nstime_t nonrecentendbound;
+} LMTraceIDNode;
+
 #ifdef __cplusplus
 }
 #endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/libmseed.h 
new/libmseed-3.5.3/libmseed.h
--- old/libmseed-3.5.1/libmseed.h       2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/libmseed.h       2026-07-30 18:15:35.000000000 +0200
@@ -29,8 +29,8 @@
 {
 #endif
 
-#define LIBMSEED_VERSION "3.5.1"    //!< Library version
-#define LIBMSEED_RELEASE "2026.206" //!< Library release date
+#define LIBMSEED_VERSION "3.5.3"    //!< Library version
+#define LIBMSEED_RELEASE "2026.211" //!< Library release date
 
 /** @defgroup io-functions File and URL I/O */
 /** @defgroup miniseed-record Record Handling */
@@ -146,8 +146,8 @@
 #define LM_SIDLEN 64 //!< Length of source ID string
 
 /** @def MS_ISRATETOLERABLE
-    @brief Macro to test default sample rate tolerance: abs(1-sr1/sr2) < 
0.0001 */
-#define MS_ISRATETOLERABLE(A, B) (fabs (1.0 - ((A) / (B))) < 0.0001)
+    @brief Macro to test default sample rate tolerance: abs(sr2-sr1) < 
0.0001*abs(sr2) */
+#define MS_ISRATETOLERABLE(A, B) (fabs ((B) - (A)) < 0.0001 * fabs (B))
 
 /** @def MS2_ISDATAINDICATOR
     @brief Macro to test a character for miniSEED 2.x data record/quality 
indicators */
@@ -1137,6 +1137,8 @@
     the ::MSF_RECORDLIST flag to @ref mstl3_readbuffer() and @ref
     ms3_readtracelist().  Alternatively, a record list can be built by
     adding records to a @ref trace-list using mstl3_addmsr_recordptr().
+    Extra headers are copied into each ::MS3RecordPtr by default; set
+    ::MSF_RECORDLIST_NOEXTRAS to omit them, usually to reduce memory usage.
 
     The main purpose of this functionality is to support an efficient,
     2-pass pattern of first reading a summary of data followed by
@@ -1636,6 +1638,7 @@
 #define MSF_PPUPDATETIME 0x0400 //!< [TraceList] Store update time (as 
nstime_t) at ::MS3TraceSeg.prvtptr
 #define MSF_SPLITISVERSION 0x0800 //!< [TraceList] Use the splitversion value 
as version instead of record version
 #define MSF_SKIPADJACENTDUPLICATES 0x1000 //!< [TraceList] Skip adjacent 
duplicate records
+#define MSF_RECORDLIST_NOEXTRAS 0x2000 //!< [TraceList] Do not copy extra 
headers to the record list
 /** @} */
 
 #ifdef __cplusplus
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/parseutils.c 
new/libmseed-3.5.3/parseutils.c
--- old/libmseed-3.5.1/parseutils.c     2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/parseutils.c     2026-07-30 18:15:35.000000000 +0200
@@ -235,7 +235,8 @@
 
     /* Loop through blockettes as long as number is non-zero and the 4-byte
      * blockette header (type and next offset) is fully within the buffer */
-    while (blkt_offset != 0 && blkt_offset > 47 && (uint64_t)blkt_offset + 4 
<= recbuflen)
+    while (blkt_offset != 0 && blkt_offset >= MS2FSDH_LENGTH &&
+           (uint64_t)blkt_offset + 4 <= recbuflen)
     {
       memcpy (&blkt_type, record + blkt_offset, 2);
       memcpy (&next_blkt, record + blkt_offset + 2, 2);
@@ -850,6 +851,7 @@
       {
         ms_log (2, "%s: Unknown blockette length for type %d\n", sid, 
blkt_type);
         retval++;
+        break;
       }
 
       /* Track end of blockette chain */
@@ -1397,7 +1399,7 @@
       }
 
       /* Sanity check the next blockette offset */
-      if (next_blkt && next_blkt <= endofblockettes)
+      if (next_blkt && (next_blkt == blkt_offset || next_blkt <= 
endofblockettes))
       {
         ms_log (2, "%s: Next blockette offset (%d) is within current blockette 
ending at byte %d\n",
                 sid, next_blkt, endofblockettes);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/test/test-read.c 
new/libmseed-3.5.3/test/test-read.c
--- old/libmseed-3.5.1/test/test-read.c 2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/test/test-read.c 2026-07-30 18:15:35.000000000 +0200
@@ -623,9 +623,10 @@
   CHECK (int32s[3631] == 26, "Decoded sample value mismatch");
   ms3_readmsr(&msr, NULL, flags, 0);
 
-  /* Invalid blockette chain (format version 2).  One could argue these should 
not be readable. */
+  /* Invalid blockette chain (format version 2): FSDH blockette offset (40)
+   * falls within the 48-byte fixed header, not a valid blockette start. */
   rv = ms3_readmsr (&msr, "data/testdata-invalid-blockette-offsets.mseed2", 
flags, 0);
-  REQUIRE (rv == MS_NOERROR, "ms3_readmsr() did not return expected 
MS_NOERROR");
+  REQUIRE (rv == MS_GENERROR, "ms3_readmsr() did not return expected 
MS_GENERROR");
   ms3_readmsr (&msr, NULL, flags, 0);
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/test/test-tracelist.c 
new/libmseed-3.5.3/test/test-tracelist.c
--- old/libmseed-3.5.1/test/test-tracelist.c    2026-07-25 22:25:49.000000000 
+0200
+++ new/libmseed-3.5.3/test/test-tracelist.c    2026-07-30 18:15:35.000000000 
+0200
@@ -204,6 +204,59 @@
   mstl3_free (&mstl, 1);
 }
 
+/* This test adds a header-only record with extra headers to a MS3TraceList
+ * via mstl3_addmsr_recordptr(), once with default flags and once with the
+ * MSF_RECORDLIST_NOEXTRAS flag set, verifying that the extra headers are
+ * copied to the resulting MS3RecordPtr by default and omitted when the flag
+ * is set.
+ */
+TEST (tracelist, mstl3_addmsr_recordptr_noextras)
+{
+  MS3TraceList *mstl   = NULL;
+  MS3RecordPtr *recptr = NULL;
+  MS3Record msr = MS3Record_INITIALIZER;
+  char *extra = "{\"FDSN\":{\"Time\":{\"Quality\":100}}}";
+
+  strcpy (msr.sid, "FDSN:XX_TEST__X_H_Z");
+  msr.formatversion = 3;
+  msr.pubversion = 1;
+  msr.starttime = ms_timestr2nstime ("2024-01-01T00:00:00.0Z");
+  msr.samprate = 1.0;
+  msr.sampletype = 'i';
+  msr.samplecnt = 100;
+  msr.numsamples = 0;
+  msr.datasamples = NULL;
+  msr.extra = extra;
+  msr.extralength = (uint16_t)strlen (extra);
+
+  /* Default flags: extra headers are copied to the record list entry */
+  REQUIRE (mstl = mstl3_init (NULL), "mstl3_init() returned unexpected NULL");
+  REQUIRE (mstl3_addmsr_recordptr (mstl, &msr, &recptr, 0, 1, 0, NULL) != NULL,
+           "mstl3_addmsr_recordptr() returned unexpected NULL");
+
+  REQUIRE (recptr != NULL, "recptr is unexpected NULL");
+  REQUIRE (recptr->msr != NULL, "recptr->msr is unexpected NULL");
+  CHECK (recptr->msr->extralength == msr.extralength,
+         "recptr->msr->extralength does not match source extralength");
+  REQUIRE (recptr->msr->extra != NULL, "recptr->msr->extra is unexpected 
NULL");
+  CHECK_STREQ (recptr->msr->extra, extra);
+
+  mstl3_free (&mstl, 0);
+
+  /* MSF_RECORDLIST_NOEXTRAS: extra headers are not copied */
+  recptr = NULL;
+  REQUIRE (mstl = mstl3_init (NULL), "mstl3_init() returned unexpected NULL");
+  REQUIRE (mstl3_addmsr_recordptr (mstl, &msr, &recptr, 0, 1, 
MSF_RECORDLIST_NOEXTRAS, NULL) != NULL,
+           "mstl3_addmsr_recordptr() returned unexpected NULL");
+
+  REQUIRE (recptr != NULL, "recptr is unexpected NULL");
+  REQUIRE (recptr->msr != NULL, "recptr->msr is unexpected NULL");
+  CHECK (recptr->msr->extralength == 0, "recptr->msr->extralength is not 
expected 0");
+  CHECK (recptr->msr->extra == NULL, "recptr->msr->extra is not expected 
NULL");
+
+  mstl3_free (&mstl, 0);
+}
+
 /* This test reads miniSEED from a file into a MS3TraceList while using the
  * MSF_PPUPDATETIME flag to set the segment prvtptr to the update time of the
  * record.  The expected value of the segment prvtptr is verified to be within
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/tracelist.c 
new/libmseed-3.5.3/tracelist.c
--- old/libmseed-3.5.1/tracelist.c      2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/tracelist.c      2026-07-30 18:15:35.000000000 +0200
@@ -27,12 +27,23 @@
 #include "internalstate.h"
 #include "libmseed.h"
 
+static MS3TraceID *lm_addID (MS3TraceList *mstl, MS3TraceID *id, MS3TraceID 
**prev);
 static MS3TraceSeg *lm_msr2seg (const MS3Record *msr, nstime_t endtime);
 static MS3TraceSeg *lm_addmsrtoseg (MS3TraceSeg *seg, const MS3Record *msr, 
nstime_t endtime,
                                     int8_t whence);
 static MS3TraceSeg *lm_addsegtoseg (MS3TraceSeg *seg1, MS3TraceSeg *seg2);
 static MS3RecordPtr *lm_add_recordptr (MS3TraceSeg *seg, const MS3Record *msr, 
nstime_t endtime,
-                                       int8_t whence);
+                                       int8_t whence, uint32_t flags);
+
+static void lm_recentseg_touch (LMTraceIDNode *idnode, MS3TraceSeg *seg);
+static void lm_recentseg_remove (LMTraceIDNode *idnode, MS3TraceSeg *seg);
+static void lm_endbound_fold (LMTraceIDNode *idnode, nstime_t endtime);
+static int lm_seg_listorder (const MS3TraceSeg *a, const MS3TraceSeg *b, int 
*order);
+static int lm_scan_recent (LMTraceIDNode *idnode, const MS3Record *msr, 
nstime_t endtime,
+                           nstime_t nsperiod, nstime_t nstimetol, nstime_t 
nnstimetol,
+                           double sampratehz, double sampratetol, int8_t 
autoheal,
+                           MS3TraceSeg **psegbefore, MS3TraceSeg **psegafter,
+                           MS3TraceSeg **pfollowseg);
 
 static void lm_free_segment_memory (MS3TraceSeg *seg, int8_t freeprvtptr);
 static int lm_remove_segment (MS3TraceList *mstl, MS3TraceID *id, MS3TraceSeg 
*seg,
@@ -45,7 +56,7 @@
 /* Test if two sample rates are similar using either specified tolerance (if 
non-negative) or
  * default tolerance */
 #define IS_SAMPRATE_SIMILAR(SR1, SR2, SRT) \
-  ((SRT >= 0.0) ? fabs (SR1 - SR2) <= SRT : MS_ISRATETOLERABLE (SR1, SR2))
+  ((SR1) == (SR2) || ((SRT >= 0.0) ? fabs (SR1 - SR2) <= SRT : 
MS_ISRATETOLERABLE (SR1, SR2)))
 
 /* Test if a MS3TraceSeg represents time coverage */
 #define SEGMENT_HAS_TIME_COVERAGE(seg) ((seg)->samplecnt > 0 && 
(seg)->samprate != 0.0)
@@ -73,7 +84,7 @@
     mstl3_free (&mstl, 1);
   }
 
-  mstl = (MS3TraceList *)libmseed_memory.malloc (sizeof (MS3TraceList));
+  mstl = (MS3TraceList *)libmseed_memory.malloc (sizeof (LMTraceListNode));
 
   if (mstl == NULL)
   {
@@ -81,7 +92,7 @@
     return NULL;
   }
 
-  memset (mstl, 0, sizeof (MS3TraceList));
+  memset (mstl, 0, sizeof (LMTraceListNode));
 
   /* Seed PRNG with 1, we only need random distribution */
   mstl->prngstate = 1;
@@ -236,6 +247,19 @@
 MS3TraceID *
 mstl3_addID (MS3TraceList *mstl, MS3TraceID *id, MS3TraceID **prev)
 {
+  /* An ID added through this public entry point may not carry the private
+   * tail this library allocates internally, so disable the state that
+   * relies on it for the containing list. */
+  if (mstl)
+    ((LMTraceListNode *)mstl)->foreignid = 1;
+
+  return lm_addID (mstl, id, prev);
+} /* End of mstl3_addID() */
+
+/* Add a MS3TraceID to a MS3TraceList, see mstl3_addID() for the public entry 
point. */
+static MS3TraceID *
+lm_addID (MS3TraceList *mstl, MS3TraceID *id, MS3TraceID **prev)
+{
   MS3TraceID *local_prev[MSTRACEID_SKIPLIST_HEIGHT] = {NULL};
   int level;
 
@@ -281,7 +305,279 @@
   mstl->numtraceids++;
 
   return id;
-} /* End of mstl3_addID() */
+} /* End of lm_addID() */
+
+/***************************************************************************
+ * Move a segment into the most-recently-used slot of a trace ID's recent
+ * set, evicting the least-recently-used entry if the segment was not
+ * already tracked.  An evicted segment's current end time is folded into
+ * the non-recent end-time bound before it is dropped.
+ ***************************************************************************/
+static void
+lm_recentseg_touch (LMTraceIDNode *idnode, MS3TraceSeg *seg)
+{
+  MS3TraceSeg *evicted;
+  int found = -1;
+  int i;
+
+  if (!seg)
+    return;
+
+  for (i = 0; i < LM_RECENTSEGS; i++)
+  {
+    if (idnode->recentseg[i] == seg)
+    {
+      found = i;
+      break;
+    }
+  }
+
+  /* Only the least-recently-used slot is evicted, and only when the
+   * segment being touched was not already present */
+  evicted = (found < 0) ? idnode->recentseg[LM_RECENTSEGS - 1] : NULL;
+
+  /* Shift entries from the found (or last) slot down to make room at the 
front */
+  for (i = (found < 0) ? LM_RECENTSEGS - 1 : found; i > 0; i--)
+  {
+    idnode->recentseg[i] = idnode->recentseg[i - 1];
+  }
+
+  idnode->recentseg[0] = seg;
+
+  if (evicted)
+    lm_endbound_fold (idnode, evicted->endtime);
+} /* End of lm_recentseg_touch() */
+
+/***************************************************************************
+ * Remove a segment from a trace ID's recent set, if present, compacting the
+ * array.  The end-time bound is not lowered; it may now overestimate for
+ * this segment, which only costs search performance.
+ ***************************************************************************/
+static void
+lm_recentseg_remove (LMTraceIDNode *idnode, MS3TraceSeg *seg)
+{
+  int i, j;
+
+  for (i = 0; i < LM_RECENTSEGS; i++)
+  {
+    if (idnode->recentseg[i] == seg)
+    {
+      for (j = i; j < LM_RECENTSEGS - 1; j++)
+        idnode->recentseg[j] = idnode->recentseg[j + 1];
+
+      idnode->recentseg[LM_RECENTSEGS - 1] = NULL;
+      break;
+    }
+  }
+} /* End of lm_recentseg_remove() */
+
+/***************************************************************************
+ * Raise a trace ID's non-recent end-time bound to cover a known end time.
+ * The bound only ever grows.
+ ***************************************************************************/
+static void
+lm_endbound_fold (LMTraceIDNode *idnode, nstime_t endtime)
+{
+  if (endtime > idnode->nonrecentendbound)
+    idnode->nonrecentendbound = endtime;
+} /* End of lm_endbound_fold() */
+
+/***************************************************************************
+ * Determine the list order of two segments: starttime ascending, endtime
+ * descending.  Segments with fully equal keys are adjacent in the list;
+ * such ties are resolved by walking a bounded number of hops from 'a' in
+ * each direction looking for 'b'.
+ *
+ * On success returns 1 and sets *order to -1 (a before b), 0 (a is b), or
+ * 1 (a after b).  Returns 0 if the order could not be resolved within the
+ * walk bound.
+ ***************************************************************************/
+static int
+lm_seg_listorder (const MS3TraceSeg *a, const MS3TraceSeg *b, int *order)
+{
+  const MS3TraceSeg *walk;
+  int hops;
+
+  if (a == b)
+  {
+    *order = 0;
+    return 1;
+  }
+
+  if (a->starttime != b->starttime)
+  {
+    *order = (a->starttime < b->starttime) ? -1 : 1;
+    return 1;
+  }
+
+  if (a->endtime != b->endtime)
+  {
+    *order = (a->endtime > b->endtime) ? -1 : 1;
+    return 1;
+  }
+
+  for (walk = a->next, hops = 0; walk && hops < LM_RECENTSEGS_MAXWALK; walk = 
walk->next, hops++)
+  {
+    if (walk == b)
+    {
+      *order = -1;
+      return 1;
+    }
+  }
+
+  for (walk = a->prev, hops = 0; walk && hops < LM_RECENTSEGS_MAXWALK; walk = 
walk->prev, hops++)
+  {
+    if (walk == b)
+    {
+      *order = 1;
+      return 1;
+    }
+  }
+
+  return 0;
+} /* End of lm_seg_listorder() */
+
+/***************************************************************************
+ * Reproduce the segment-list search of _mstl3_addmsr_impl() using only
+ * the recent segments of a trace ID.  Callable only when every segment not
+ * in the recent set is provably out of range for segbefore, segafter, and
+ * the autoheal exact match, and necessarily sorts before the record (see
+ * the guard at the call site); under that condition the outcome of the
+ * recent segments alone matches a full scan.
+ *
+ * This loop body mirrors the general search in _mstl3_addmsr_impl() and
+ * must be kept in lockstep with it.
+ *
+ * Returns 1 with *psegbefore, *psegafter and *pfollowseg set (any may be
+ * NULL) when the shortcut applies. Returns 0, with the outputs untouched,
+ * when the search cannot be resolved from the recent set and the caller
+ * must fall back to a full scan.
+ ***************************************************************************/
+static int
+lm_scan_recent (LMTraceIDNode *idnode, const MS3Record *msr, nstime_t endtime, 
nstime_t nsperiod,
+                nstime_t nstimetol, nstime_t nnstimetol, double sampratehz, 
double sampratetol,
+                int8_t autoheal, MS3TraceSeg **psegbefore, MS3TraceSeg 
**psegafter,
+                MS3TraceSeg **pfollowseg)
+{
+  MS3TraceSeg *recent[LM_RECENTSEGS];
+  MS3TraceSeg *searchseg;
+  MS3TraceSeg *segbefore = NULL;
+  MS3TraceSeg *segafter = NULL;
+  MS3TraceSeg *followseg = NULL;
+  nstime_t postgap;
+  nstime_t pregap;
+  int order;
+  int count = 0;
+  int i, j;
+
+  /* Collect the tracked recent segments */
+  for (i = 0; i < LM_RECENTSEGS; i++)
+  {
+    if (idnode->recentseg[i])
+      recent[count++] = idnode->recentseg[i];
+  }
+
+  /* Sort the collected segments into list order; a small insertion sort
+   * is sufficient for this fixed, small-size array */
+  for (i = 1; i < count; i++)
+  {
+    for (j = i; j > 0; j--)
+    {
+      if (!lm_seg_listorder (recent[j - 1], recent[j], &order))
+        return 0;
+
+      if (order <= 0)
+        break;
+
+      searchseg = recent[j - 1];
+      recent[j - 1] = recent[j];
+      recent[j] = searchseg;
+    }
+  }
+
+  /* Identical loop body to the general search in _mstl3_addmsr_impl(),
+   * run over the recent segments only, in list order */
+  for (i = 0; i < count; i++)
+  {
+    searchseg = recent[i];
+
+    /* Done searching when segment starts beyond the record end plus tolerance 
*/
+    if (searchseg->starttime > endtime + nsperiod + nstimetol)
+      break;
+
+    /* Skip segments with no time coverage, these cannot be extended */
+    if (!SEGMENT_HAS_TIME_COVERAGE (searchseg))
+      continue;
+
+    /* Done searching if autohealing and record exactly matches a segment */
+    if (autoheal && msr->starttime == searchseg->starttime && endtime == 
searchseg->endtime)
+    {
+      followseg = searchseg;
+      break;
+    }
+
+    if (msr->starttime > searchseg->starttime)
+      followseg = searchseg;
+
+    if (!segbefore)
+    {
+      postgap = msr->starttime - searchseg->endtime - nsperiod;
+
+      if (postgap <= nstimetol && postgap >= nnstimetol &&
+          IS_SAMPRATE_SIMILAR (sampratehz, searchseg->samprate, sampratetol))
+        segbefore = searchseg;
+    }
+
+    if (!segafter)
+    {
+      pregap = searchseg->starttime - endtime - nsperiod;
+
+      if (pregap <= nstimetol && pregap >= nnstimetol &&
+          IS_SAMPRATE_SIMILAR (sampratehz, searchseg->samprate, sampratetol))
+        segafter = searchseg;
+    }
+
+    /* Done searching if both before and after segments are found */
+    if (segbefore && segafter)
+      break;
+    /* Done searching if not autohealing and one match found */
+    else if (!autoheal && (segbefore || segafter))
+      break;
+  }
+
+  /* If nothing matched, followseg must be the globally latest-starting
+   * coverage segment before the record, which may be outside the recent
+   * set: walk backward from the list end, skipping segments that start
+   * at/after the record (necessarily recent, already considered above) or
+   * that lack coverage. */
+  if (!segbefore && !segafter && !followseg)
+  {
+    int hops;
+
+    searchseg = idnode->id.last;
+
+    for (hops = 0; searchseg && hops < LM_RECENTSEGS_MAXWALK; hops++)
+    {
+      if (SEGMENT_HAS_TIME_COVERAGE (searchseg) && searchseg->starttime < 
msr->starttime)
+      {
+        followseg = searchseg;
+        break;
+      }
+
+      searchseg = searchseg->prev;
+    }
+
+    /* Walk bound exceeded without resolving followseg, refuse the shortcut */
+    if (!followseg && searchseg)
+      return 0;
+  }
+
+  *psegbefore = segbefore;
+  *psegafter = segafter;
+  *pfollowseg = followseg;
+
+  return 1;
+} /* End of lm_scan_recent() */
 
 /***************************************************************************
  * Implementation of MS3TraceList addition functions
@@ -338,12 +634,12 @@
   /* If no matching ID was found create new MS3TraceID and MS3TraceSeg entries 
*/
   if (!id)
   {
-    if (!(id = (MS3TraceID *)libmseed_memory.malloc (sizeof (MS3TraceID))))
+    if (!(id = (MS3TraceID *)libmseed_memory.malloc (sizeof (LMTraceIDNode))))
     {
       ms_log (2, "Error allocating memory\n");
       return NULL;
     }
-    memset (id, 0, sizeof (MS3TraceID));
+    memset (id, 0, sizeof (LMTraceIDNode));
 
     /* Populate MS3TraceID */
     memcpy (id->sid, msr->sid, sizeof (id->sid));
@@ -352,6 +648,9 @@
     id->latest = endtime;
     id->numsegments = 1;
 
+    /* End-time bound starts below any possible end time, recent set starts 
empty */
+    ((LMTraceIDNode *)id)->nonrecentendbound = INT64_MIN;
+
     if (!(seg = lm_msr2seg (msr, endtime)))
     {
       libmseed_memory.free (id);
@@ -360,7 +659,7 @@
     id->first = id->last = seg;
 
     /* Add MS3RecordPtr if requested */
-    if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 1)))
+    if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 1, 
flags)))
     {
       lm_free_segment_memory (seg, 0);
       libmseed_memory.free (id);
@@ -368,7 +667,7 @@
     }
 
     /* Add new MS3TraceID to MS3TraceList */
-    if (mstl3_addID (mstl, id, previd) == NULL)
+    if (lm_addID (mstl, id, previd) == NULL)
     {
       ms_log (2, "Error adding new ID to trace list\n");
       lm_free_segment_memory (seg, 0);
@@ -447,8 +746,13 @@
         id->latest = endtime;
 
       /* Add MS3RecordPtr if requested */
-      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 1)))
+      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 1, 
flags)))
+      {
+        /* seg's end time was already extended above; keep the end-time bound 
valid */
+        if (!((LMTraceListNode *)mstl)->foreignid)
+          lm_endbound_fold ((LMTraceIDNode *)id, seg->endtime);
         return NULL;
+      }
     }
     /* Record coverage is after all other coverage */
     else if ((msr->starttime - nsperiod - nstimetol) > id->latest)
@@ -466,8 +770,13 @@
         id->latest = endtime;
 
       /* Add MS3RecordPtr if requested */
-      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 0)))
+      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 0, 
flags)))
+      {
+        /* seg is already linked into the list; keep the end-time bound valid 
*/
+        if (!((LMTraceListNode *)mstl)->foreignid)
+          lm_endbound_fold ((LMTraceIDNode *)id, seg->endtime);
         return NULL;
+      }
     }
     /* Record coverage is before all other coverage */
     else if ((endtime + nsperiod + nstimetol) < id->earliest)
@@ -485,8 +794,13 @@
         id->earliest = msr->starttime;
 
       /* Add MS3RecordPtr if requested */
-      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 0)))
+      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 0, 
flags)))
+      {
+        /* seg is already linked into the list; keep the end-time bound valid 
*/
+        if (!((LMTraceListNode *)mstl)->foreignid)
+          lm_endbound_fold ((LMTraceIDNode *)id, seg->endtime);
         return NULL;
+      }
     }
     /* Record coverage fits at beginning of first segment */
     else if (firstgap <= nstimetol && firstgap >= nnstimetol &&
@@ -502,7 +816,7 @@
         id->earliest = msr->starttime;
 
       /* Add MS3RecordPtr if requested */
-      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 2)))
+      if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 2, 
flags)))
         return NULL;
     }
     /* Search complete segment list for matches */
@@ -512,57 +826,110 @@
       segbefore = NULL; /* The first segment end that matches the record start 
(within tolerance) */
       segafter = NULL;  /* The first segment start that matches the record end 
(within tolerance) */
       followseg = NULL; /* The segment with latest start time before the 
record start */
-      searchseg = id->first;
-      while (searchseg)
+
+      /* A zero rate record cannot match segbefore/segafter without an explicit
+       * tolerance, so only followseg is needed: search backward from the last 
segment. */
+      if (sampratehz == 0.0 && sampratetol < 0.0)
       {
-        /* Skip segments with no time coverage, these cannot be extended */
-        if (!SEGMENT_HAS_TIME_COVERAGE (searchseg))
+        searchseg = id->last;
+        while (searchseg)
         {
-          searchseg = searchseg->next;
-          continue;
-        }
+          /* Skip segments with no time coverage, these cannot be extended */
+          if (!SEGMENT_HAS_TIME_COVERAGE (searchseg))
+          {
+            searchseg = searchseg->prev;
+            continue;
+          }
 
-        /* Done searching if autohealing and record exactly matches a segment.
-         *
-         * Rationale: autohealing would have combined this segment
-         * with another if that were possible, so this record will
-         * also not fit with any other segment. */
-        if (autoheal && msr->starttime == searchseg->starttime && endtime == 
searchseg->endtime)
-        {
-          followseg = searchseg;
-          break;
-        }
+          /* Done searching if autohealing and record exactly matches a 
segment.
+           *
+           * Rationale: autohealing would have combined this segment
+           * with another if that were possible, so this record will
+           * also not fit with any other segment. */
+          if (autoheal && msr->starttime == searchseg->starttime && endtime == 
searchseg->endtime)
+          {
+            followseg = searchseg;
+            break;
+          }
 
-        if (msr->starttime > searchseg->starttime)
-          followseg = searchseg;
+          /* Done searching at the first (i.e. latest) segment starting before 
the record */
+          if (searchseg->starttime < msr->starttime)
+          {
+            followseg = searchseg;
+            break;
+          }
 
-        if (!segbefore)
+          searchseg = searchseg->prev;
+        }
+      }
+      /* If every segment outside the recent set is provably too old to match,
+       * the recent set alone determines the result of the search below. */
+      else if (!((LMTraceListNode *)mstl)->foreignid &&
+               (msr->starttime - nsperiod - nstimetol) > ((LMTraceIDNode 
*)id)->nonrecentendbound &&
+               lm_scan_recent ((LMTraceIDNode *)id, msr, endtime, nsperiod, 
nstimetol, nnstimetol,
+                               sampratehz, sampratetol, autoheal, &segbefore, 
&segafter,
+                               &followseg))
+      {
+        /* segbefore, segafter and followseg set by lm_scan_recent() */
+      }
+      else
+      {
+        searchseg = id->first;
+        while (searchseg)
         {
-          postgap = msr->starttime - searchseg->endtime - nsperiod;
+          /* Done searching when segment starts beyond the record end plus 
tolerance */
+          if (searchseg->starttime > endtime + nsperiod + nstimetol)
+            break;
 
-          if (postgap <= nstimetol && postgap >= nnstimetol &&
-              IS_SAMPRATE_SIMILAR (sampratehz, searchseg->samprate, 
sampratetol))
-            segbefore = searchseg;
-        }
+          /* Skip segments with no time coverage, these cannot be extended */
+          if (!SEGMENT_HAS_TIME_COVERAGE (searchseg))
+          {
+            searchseg = searchseg->next;
+            continue;
+          }
 
-        if (!segafter)
-        {
-          pregap = searchseg->starttime - endtime - nsperiod;
+          /* Done searching if autohealing and record exactly matches a 
segment.
+           *
+           * Rationale: autohealing would have combined this segment
+           * with another if that were possible, so this record will
+           * also not fit with any other segment. */
+          if (autoheal && msr->starttime == searchseg->starttime && endtime == 
searchseg->endtime)
+          {
+            followseg = searchseg;
+            break;
+          }
 
-          if (pregap <= nstimetol && pregap >= nnstimetol &&
-              IS_SAMPRATE_SIMILAR (sampratehz, searchseg->samprate, 
sampratetol))
-            segafter = searchseg;
-        }
+          if (msr->starttime > searchseg->starttime)
+            followseg = searchseg;
 
-        /* Done searching if both before and after segments are found */
-        if (segbefore && segafter)
-          break;
-        /* Done searching if not autohealing and one match found */
-        else if (!autoheal && (segbefore || segafter))
-          break;
+          if (!segbefore)
+          {
+            postgap = msr->starttime - searchseg->endtime - nsperiod;
 
-        searchseg = searchseg->next;
-      } /* Done looping through segments */
+            if (postgap <= nstimetol && postgap >= nnstimetol &&
+                IS_SAMPRATE_SIMILAR (sampratehz, searchseg->samprate, 
sampratetol))
+              segbefore = searchseg;
+          }
+
+          if (!segafter)
+          {
+            pregap = searchseg->starttime - endtime - nsperiod;
+
+            if (pregap <= nstimetol && pregap >= nnstimetol &&
+                IS_SAMPRATE_SIMILAR (sampratehz, searchseg->samprate, 
sampratetol))
+              segafter = searchseg;
+          }
+
+          /* Done searching if both before and after segments are found */
+          if (segbefore && segafter)
+            break;
+          /* Done searching if not autohealing and one match found */
+          else if (!autoheal && (segbefore || segafter))
+            break;
+
+          searchseg = searchseg->next;
+        } /* Done looping through segments */
+      }
 
       /* Add MS3Record coverage to end of segment before */
       if (segbefore)
@@ -573,8 +940,11 @@
         }
 
         /* Add MS3RecordPtr if requested */
-        if (pprecptr && !(*pprecptr = lm_add_recordptr (segbefore, msr, 
endtime, 1)))
+        if (pprecptr && !(*pprecptr = lm_add_recordptr (segbefore, msr, 
endtime, 1, flags)))
         {
+          /* segbefore's end time was already extended above; keep the 
end-time bound valid */
+          if (!((LMTraceListNode *)mstl)->foreignid)
+            lm_endbound_fold ((LMTraceIDNode *)id, segbefore->endtime);
           return NULL;
         }
 
@@ -584,6 +954,8 @@
           /* Add segafter coverage to segbefore */
           if (!lm_addsegtoseg (segbefore, segafter))
           {
+            if (!((LMTraceListNode *)mstl)->foreignid)
+              lm_endbound_fold ((LMTraceIDNode *)id, segbefore->endtime);
             return NULL;
           }
 
@@ -597,6 +969,10 @@
           if (segafter->next)
             segafter->next->prev = segafter->prev;
 
+          /* Drop segafter from the recent set before it is freed */
+          if (!((LMTraceListNode *)mstl)->foreignid)
+            lm_recentseg_remove ((LMTraceIDNode *)id, segafter);
+
           /* Free all memory associated with the segment after that has been 
merged */
           lm_free_segment_memory (segafter, 1);
 
@@ -614,7 +990,7 @@
         }
 
         /* Add MS3RecordPtr if requested */
-        if (pprecptr && !(*pprecptr = lm_add_recordptr (segafter, msr, 
endtime, 2)))
+        if (pprecptr && !(*pprecptr = lm_add_recordptr (segafter, msr, 
endtime, 2, flags)))
         {
           return NULL;
         }
@@ -631,7 +1007,7 @@
         }
 
         /* Add MS3RecordPtr if requested */
-        if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 0)))
+        if (pprecptr && !(*pprecptr = lm_add_recordptr (seg, msr, endtime, 0, 
flags)))
         {
           /* seg is not yet linked into the segment list, free it directly */
           lm_free_segment_memory (seg, 0);
@@ -728,6 +1104,13 @@
       id->last = segbefore;
   }
 
+  /* Track the most-recently-active segments to bound future searches */
+  if (seg && !((LMTraceListNode *)mstl)->foreignid)
+  {
+    lm_recentseg_touch ((LMTraceIDNode *)id, seg);
+    lm_recentseg_touch ((LMTraceIDNode *)id, id->last);
+  }
+
   /* Store update time at seg.prvtptr, allocate if needed */
   if (seg && flags & MSF_PPUPDATETIME)
   {
@@ -793,6 +1176,7 @@
  * @parblock
  *  - @c ::MSF_PPUPDATETIME : Store update time (as nstime_t) at 
::MS3TraceSeg.prvtptr
  *  - @c ::MSF_SPLITISVERSION : Use @p splitversion as the version, otherwise 
use msr->pubversion
+ *  - @c ::MSF_RECORDLIST_NOEXTRAS : Do not copy extra headers into record 
list entries
  * @endparblock
  * @param[in] tolerance Tolerance function pointers as ::MS3Tolerance
  *
@@ -847,6 +1231,7 @@
  * If the ::MSF_RECORDLIST flag is set in @p flags, a ::MS3RecordList
  * will be built for each ::MS3TraceSeg.  The ::MS3RecordPtr entries
  * contain the location of the data record, bit flags, extra headers, etc.
+ * Extra headers are omitted if ::MSF_RECORDLIST_NOEXTRAS is also set.
  *
  * @param[in] ppmstl Pointer-to-point to destination MS3TraceList
  * @param[in] buffer Source buffer to read miniSEED records from
@@ -855,6 +1240,7 @@
  * @param[in] flags Flags to control parsing and optional functionality:
  * @parblock
  *  - @c ::MSF_RECORDLIST : Build a ::MS3RecordList for each ::MS3TraceSeg
+ *  - @c ::MSF_RECORDLIST_NOEXTRAS : Do not copy extra headers into record 
list entries
  *  - Flags supported by msr3_parse()
  *  - Flags supported by mstl3_addmsr()
  * @endparblock
@@ -890,6 +1276,7 @@
  * If the ::MSF_RECORDLIST flag is set in @p flags, a ::MS3RecordList
  * will be built for each ::MS3TraceSeg.  The ::MS3RecordPtr entries
  * contain the location of the data record, bit flags, extra headers, etc.
+ * Extra headers are omitted if ::MSF_RECORDLIST_NOEXTRAS is also set.
  *
  * If @p selections is not NULL, the ::MS3Selections will be used to
  * limit what is returned to the caller.  Any data not matching the
@@ -902,6 +1289,7 @@
  * @param[in] flags Flags to control parsing and optional functionality:
  * @parblock
  *  - @c ::MSF_RECORDLIST : Build a ::MS3RecordList for each ::MS3TraceSeg
+ *  - @c ::MSF_RECORDLIST_NOEXTRAS : Do not copy extra headers into record 
list entries
  *  - Flags supported by msr3_parse()
  *  - Flags supported by mstl3_addmsr()
  * @endparblock
@@ -1356,7 +1744,8 @@
  * @see mstl3_addmsr()
  ***************************************************************************/
 static MS3RecordPtr *
-lm_add_recordptr (MS3TraceSeg *seg, const MS3Record *msr, nstime_t endtime, 
int8_t whence)
+lm_add_recordptr (MS3TraceSeg *seg, const MS3Record *msr, nstime_t endtime, 
int8_t whence,
+                  uint32_t flags)
 {
   MS3RecordPtr *recordptr = NULL;
 
@@ -1381,7 +1770,7 @@
   }
 
   memset (recordptr, 0, sizeof (MS3RecordPtr));
-  recordptr->msr = msr3_duplicate (msr, 0);
+  recordptr->msr = msr3_duplicate_extra (msr, 0, (flags & 
MSF_RECORDLIST_NOEXTRAS) ? 0 : 1);
   recordptr->endtime = endtime;
 
   if (recordptr->msr == NULL)
@@ -1402,6 +1791,7 @@
     if (seg->recordlist == NULL)
     {
       ms_log (2, "Cannot allocate memory\n");
+      msr3_free (&recordptr->msr);
       libmseed_memory.free (recordptr);
       return NULL;
     }
@@ -1615,7 +2005,17 @@
   /* Convert to 64-bit doubles */
   else if (type == 'd')
   {
-    if (!(ddata = (double *)libmseed_memory.malloc ((size_t)(seg->numsamples * 
sizeof (double)))))
+    size_t datasize;
+
+    if (seg->numsamples < 0 || (uint64_t)seg->numsamples > SIZE_MAX / sizeof 
(double))
+    {
+      ms_log (2, "Data buffer size overflow for %" PRId64 " samples\n", 
seg->numsamples);
+      return -1;
+    }
+
+    datasize = (size_t)seg->numsamples * sizeof (double);
+
+    if (!(ddata = (double *)libmseed_memory.malloc (datasize)))
     {
       ms_log (2, "Cannot allocate buffer for sample conversion to doubles\n");
       return -1;
@@ -1637,7 +2037,7 @@
     }
 
     seg->datasamples = ddata;
-    seg->datasize = seg->numsamples * sizeof (double);
+    seg->datasize = datasize;
     seg->sampletype = 'd';
   } /* Done converting to 64-bit doubles */
 
@@ -3422,6 +3822,10 @@
   /* Decrement segment count */
   id->numsegments -= 1;
 
+  /* Drop the segment from the recent set before it is freed */
+  if (!((LMTraceListNode *)mstl)->foreignid)
+    lm_recentseg_remove ((LMTraceIDNode *)id, seg);
+
   /* Free all memory associated with the segment */
   lm_free_segment_memory (seg, freeprvtptr);
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libmseed-3.5.1/unpack.c new/libmseed-3.5.3/unpack.c
--- old/libmseed-3.5.1/unpack.c 2026-07-25 22:25:49.000000000 +0200
+++ new/libmseed-3.5.3/unpack.c 2026-07-30 18:15:35.000000000 +0200
@@ -456,6 +456,14 @@
 
   while ((blkt_offset != 0) && ((blkt_offset + 4) <= reclen) && (blkt_offset < 
MAXRECLEN))
   {
+    /* Reject an offset within the fixed section of the header */
+    if (blkt_offset < MS2FSDH_LENGTH)
+    {
+      ms_log (2, "%s: Blockette offset (%d) is within the fixed header, 
impossible\n", msr->sid,
+              blkt_offset);
+      goto error_return;
+    }
+
     /* Every blockette has a similar 4 byte header: type and next */
     memcpy (&blkt_type, record + blkt_offset, 2);
     memcpy (&next_blkt, record + blkt_offset + 2, 2);
@@ -472,7 +480,7 @@
     {
       ms_log (2, "%s: Blockette 2000 length field extends beyond record size, 
truncated?\n",
               msr->sid);
-      break;
+      goto error_return;
     }
 
     /* Get blockette length */
@@ -481,14 +489,27 @@
     if (blkt_length == 0)
     {
       ms_log (2, "%s: Unknown blockette length for type %d\n", msr->sid, 
blkt_type);
-      break;
+
+      /* A terminated blockette chain with unknown blockette stops here */
+      if (next_blkt == 0)
+        break;
+
+      /* Skip via next offset only when it advances and stays in-bounds */
+      if (next_blkt > blkt_offset && next_blkt >= MS2FSDH_LENGTH && next_blkt 
<= reclen)
+      {
+        blkt_offset = next_blkt;
+        blkt_count++;
+        continue;
+      }
+
+      goto error_return;
     }
 
     /* Make sure blockette is contained within the msrecord buffer */
     if ((blkt_offset + blkt_length) > reclen)
     {
       ms_log (2, "%s: Blockette %d extends beyond record size, truncated?\n", 
msr->sid, blkt_type);
-      break;
+      goto error_return;
     }
 
     blkt_end = blkt_offset + blkt_length;
@@ -1287,7 +1308,7 @@
     return MS_GENERROR;
   }
 
-  /* Fallback encoding for when encoding is unknown */
+  /* Fallback encoding for when encoding is unknown (legacy bare SEED data 
records) */
   if (msr->encoding < 0)
   {
     if (verbose > 2)
@@ -1710,6 +1731,9 @@
   case 400:
     return "Beam";
     break;
+  case 405:
+    return "Beam Delay";
+    break;
   case 500:
     return "Timing";
     break;
@@ -1768,6 +1792,9 @@
   case 400: /* Beam */
     blktlen = 16;
     break;
+  case 405: /* Beam Delay */
+    blktlen = 6;
+    break;
   case 500: /* Timing */
     blktlen = 200;
     break;

Reply via email to