Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libmseed for openSUSE:Factory checked in at 2025-07-30 11:43:53 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libmseed (Old) and /work/SRC/openSUSE:Factory/.libmseed.new.13279 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libmseed" Wed Jul 30 11:43:53 2025 rev:3 rq:1296200 version:3.1.6 Changes: -------- --- /work/SRC/openSUSE:Factory/libmseed/libmseed.changes 2025-04-26 22:25:41.987221554 +0200 +++ /work/SRC/openSUSE:Factory/.libmseed.new.13279/libmseed.changes 2025-07-30 11:45:07.690103556 +0200 @@ -1,0 +2,21 @@ +Mon Jul 28 18:29:35 UTC 2025 - Andreas Stieger <andreas.stie...@gmx.de> + +- update to 3.1.6: + Use cases creating miniSEED v2 records with microsecond + resolution and non-integer sampling rates are strongly encouraged + to update to this release to retain full microsecond accuracy in + created data. + * Properly propagate sub-fractional microseconds to v2 blockette + 1001 when packing more than one record. + * Round sub-fractional and sub-microsecond times for v2 record + fields to retain accuracy. + * Ensure microsecond offsets in v2 record fields are -50 to +49, + which matches previous major library version behavior and SEED + recommendation. + * Add msr3_repack_mseed2() to repack v2 records efficiently + without decoding data samples. + * Add -s (start time) and -R (sample rate) options to + example/lm_pack.c. + * Fix some pedantic compiler warnings in msio.c. + +------------------------------------------------------------------- Old: ---- libmseed-3.1.5.tar.gz New: ---- libmseed-3.1.6.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libmseed.spec ++++++ --- /var/tmp/diff_new_pack.q4KQyZ/_old 2025-07-30 11:45:08.818150298 +0200 +++ /var/tmp/diff_new_pack.q4KQyZ/_new 2025-07-30 11:45:08.822150464 +0200 @@ -18,7 +18,7 @@ %define sover 3 Name: libmseed -Version: 3.1.5 +Version: 3.1.6 Release: 0 Summary: MiniSEED data format library License: Apache-2.0 ++++++ libmseed-3.1.5.tar.gz -> libmseed-3.1.6.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/.gitignore new/libmseed-3.1.6/.gitignore --- old/libmseed-3.1.5/.gitignore 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/.gitignore 2025-07-27 02:05:46.000000000 +0200 @@ -1,4 +1,5 @@ # C build objects +*.a *.so *.lo *.o diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/ChangeLog new/libmseed-3.1.6/ChangeLog --- old/libmseed-3.1.5/ChangeLog 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/ChangeLog 2025-07-27 02:05:46.000000000 +0200 @@ -1,3 +1,14 @@ +2025.207: 3.1.6 + - Properly propagate sub-fractional microseconds to v2 blockette 1001 + when packing more than one record. + - Round sub-fractional and sub-microsecond times for v2 record fields + to retain accuracy. + - Ensure microsecond offsets in v2 record fields are -50 to +49, which + matches previous major library version behavior and SEED recommendation. + - Add msr3_repack_mseed2() to repack v2 records efficiently. + - Add -s and -R options to example/lm_pack.c. + - Fix some pedantic compiler warnings in msio.c. + 2025.114: 3.1.5 - Add new functions to libmseed.map and libmseed.def for public use. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/example/lm_pack.c new/libmseed-3.1.6/example/lm_pack.c --- old/libmseed-3.1.5/example/lm_pack.c 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/example/lm_pack.c 2025-07-27 02:05:46.000000000 +0200 @@ -29,11 +29,13 @@ #define VERSION "[libmseed " LIBMSEED_VERSION " example]" #define PACKAGE "lm_pack" -static flag verbose = 0; -static int reclen = -1; -static int encoding = 10; -static int msformat = 3; -static char *outfile = NULL; +static flag verbose = 0; +static int reclen = -1; +static double samprate = 1.0; +static int encoding = 10; +static int msformat = 3; +static nstime_t starttime = NSTUNSET; +static char *outfile = NULL; static int parameter_proc (int argcount, char **argvec); static void usage (void); @@ -171,6 +173,8 @@ if (parameter_proc (argc, argv) < 0) return -1; + if (starttime == NSTUNSET) + starttime = ms_timestr2nstime ("2012-01-01T00:00:00Z"); if (!(msr = msr3_init (msr))) { @@ -182,8 +186,8 @@ strcpy (msr->sid, "FDSN:XX_TEST__X_Y_Z"); msr->reclen = reclen; msr->pubversion = 1; - msr->starttime = ms_timestr2nstime ("2012-01-01T00:00:00"); - msr->samprate = 1.0; + msr->starttime = starttime; + msr->samprate = samprate; msr->encoding = encoding; if (encoding == DE_TEXT) @@ -292,10 +296,24 @@ { reclen = strtol (argvec[++optind], NULL, 10); } + else if (strcmp (argvec[optind], "-R") == 0) + { + samprate = strtod (argvec[++optind], NULL); + } else if (strcmp (argvec[optind], "-e") == 0) { encoding = strtol (argvec[++optind], NULL, 10); } + else if (strcmp (argvec[optind], "-s") == 0) + { + starttime = ms_timestr2nstime (argvec[++optind]); + + if (starttime == NSTERROR) + { + ms_log (2, "Invalid start time: %s\n", argvec[optind]); + exit (1); + } + } else if (strcmp (argvec[optind], "-o") == 0) { outfile = argvec[++optind]; @@ -344,7 +362,9 @@ " -v Be more verbose, multiple flags can be used\n" " -F format Specify miniSEED version format, default is v3\n" " -r bytes Specify maximum record length in bytes, default 4096\n" + " -R samprate Specify sample rate, default is 1.0\n" " -e encoding Specify encoding format\n" + " -s starttime Specify the start time, default is 2012-01-01T00:00:00Z\n" "\n" " -o outfile Specify the output file, required\n" "\n" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/genutils.c new/libmseed-3.1.6/genutils.c --- old/libmseed-3.1.5/genutils.c 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/genutils.c 2025-07-27 02:05:46.000000000 +0200 @@ -1735,16 +1735,19 @@ /**********************************************************************/ /** * @brief Runtime test for host endianess - * @returns 0 if the host is little endian, otherwise 1. + * @returns 1 if the host is big endian, 0 otherwise. ***************************************************************************/ inline int ms_bigendianhost (void) { - const uint16_t endian = 256; - return *(const uint8_t *)&endian; + union + { + uint16_t word; + uint8_t bytes[2]; + } test = {0x0102}; + return (test.bytes[0] == 0x01); } /* End of ms_bigendianhost() */ - /**********************************************************************/ /** * @brief Read leap second file specified by an environment variable * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/libmseed.h new/libmseed-3.1.6/libmseed.h --- old/libmseed-3.1.5/libmseed.h 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/libmseed.h 2025-07-27 02:05:46.000000000 +0200 @@ -28,8 +28,8 @@ extern "C" { #endif -#define LIBMSEED_VERSION "3.1.5" //!< Library version -#define LIBMSEED_RELEASE "2025.114" //!< Library release date +#define LIBMSEED_VERSION "3.1.6" //!< Library version +#define LIBMSEED_RELEASE "2025.207" //!< Library release date /** @defgroup io-functions File and URL I/O */ /** @defgroup miniseed-record Record Handling */ @@ -418,6 +418,8 @@ extern int msr3_repack_mseed3 (const MS3Record *msr, char *record, uint32_t recbuflen, int8_t verbose); +extern int msr3_repack_mseed2 (const MS3Record *msr, char *record, uint32_t recbuflen, int8_t verbose); + extern int msr3_pack_header3 (const MS3Record *msr, char *record, uint32_t recbuflen, int8_t verbose); extern int msr3_pack_header2 (const MS3Record *msr, char *record, uint32_t recbuflen, int8_t verbose); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/msio.c new/libmseed-3.1.6/msio.c --- old/libmseed-3.1.5/msio.c 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/msio.c 2025-07-27 02:05:46.000000000 +0200 @@ -22,6 +22,7 @@ #define _LARGEFILE_SOURCE 1 #include <errno.h> +#include <stddef.h> #include "msio.h" @@ -102,11 +103,11 @@ { struct header_callback_parameters *hcp = (struct header_callback_parameters *)userdata; - char startstr[21] = {0}; /* Maximum of 20 digit value */ - char endstr[21] = {0}; /* Maximum of 20 digit value */ - int startdigits = 0; - int enddigits = 0; - char *dash = NULL; + char startstr[21] = {0}; /* Maximum of 20 digit value */ + char endstr[21] = {0}; /* Maximum of 20 digit value */ + uint8_t startdigits = 0; + uint8_t enddigits = 0; + char *dash = NULL; char *ptr; if (!buffer || !userdata) @@ -119,7 +120,7 @@ if (size > 22 && strncasecmp (buffer, "Content-Range: bytes", 20) == 0) { /* Process each character, starting just afer "bytes" unit */ - for (ptr = buffer + 20; *ptr != '\0' && (ptr - buffer) < size; ptr++) + for (ptr = buffer + 20; *ptr != '\0' && (ptr - buffer) < (ptrdiff_t)size; ptr++) { /* Skip spaces before start of range */ if (*ptr == ' ' && startdigits == 0) @@ -148,10 +149,10 @@ /* Convert start and end values to numbers if non-zero length */ if (hcp->startoffset && startdigits) - *hcp->startoffset = (int64_t) strtoull (startstr, NULL, 10); + *hcp->startoffset = (int64_t)strtoull (startstr, NULL, 10); if (hcp->endoffset && enddigits) - *hcp->endoffset = (int64_t) strtoull (endstr, NULL, 10); + *hcp->endoffset = (int64_t)strtoull (endstr, NULL, 10); } return size; @@ -159,7 +160,6 @@ #endif /* defined(LIBMSEED_URL) */ - /*************************************************************************** * msio_fopen: * @@ -181,7 +181,7 @@ ***************************************************************************/ int msio_fopen (LMIO *io, const char *path, const char *mode, - int64_t *startoffset, int64_t *endoffset) + int64_t *startoffset, int64_t *endoffset) { int knownfile = 0; @@ -340,7 +340,7 @@ if (startoffset || endoffset) { hcp.startoffset = startoffset; - hcp.endoffset = endoffset; + hcp.endoffset = endoffset; /* Configure header callback */ if (curl_easy_setopt (io->handle, CURLOPT_HEADERFUNCTION, header_callback) != CURLE_OK) @@ -389,7 +389,7 @@ if ((io->handle = fopen (path, mode)) == NULL) { - ms_log (2, "Cannot open: %s (%s)\n", path, strerror(errno)); + ms_log (2, "Cannot open: %s (%s)\n", path, strerror (errno)); return -1; } @@ -405,7 +405,7 @@ } return 0; -} /* End of msio_fopen() */ +} /* End of msio_fopen() */ /********************************************************************* * msio_fclose: @@ -452,14 +452,13 @@ #endif } - io->type = LMIO_NULL; - io->handle = NULL; + io->type = LMIO_NULL; + io->handle = NULL; io->handle2 = NULL; return 0; } /* End of msio_fclose() */ - /********************************************************************* * msio_fread: * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/pack.c new/libmseed-3.1.6/pack.c --- old/libmseed-3.1.5/pack.c 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/pack.c 2025-07-27 02:05:46.000000000 +0200 @@ -41,14 +41,20 @@ void *handlerdata, int64_t *packedsamples, uint32_t flags, int8_t verbose); +static int msr3_pack_header2_offsets (const MS3Record *msr, char *record, uint32_t recbuflen, + uint16_t *blockette_1000_offset, uint16_t *blockette_1001_offset, + int8_t verbose); + static int64_t msr_pack_data (void *dest, void *src, uint64_t maxsamples, uint64_t maxdatabytes, char sampletype, int8_t encoding, int8_t swapflag, uint32_t *byteswritten, const char *sid, int8_t verbose); static int ms_genfactmult (double samprate, int16_t *factor, int16_t *multiplier); -static int64_t ms_timestr2btime (const char *timestr, uint8_t *btime, const char *sid, int8_t swapflag); +static nstime_t ms_timestr2btime (const char *timestr, uint8_t *btime, int8_t *usec_offset, + const char *sid, int8_t swapflag); +static nstime_t nstime2fsec_usec_offset (nstime_t nstime, uint16_t *fsec, int8_t *usec_offset); /**********************************************************************/ /** * @brief Pack data into miniSEED records. @@ -410,14 +416,14 @@ if (recbuflen < (MS3FSDH_LENGTH + strlen(msr->sid) + msr->extralength)) { - ms_log (2, "%s: Record length (%u) is not large enough for header (%u), SID (%"PRIsize_t"), and extra (%d)\n", + ms_log (2, "%s: Record buffer length (%u) is not large enough for header (%u), SID (%"PRIsize_t"), and extra (%d)\n", msr->sid, recbuflen, MS3FSDH_LENGTH, strlen(msr->sid), msr->extralength); return -1; } if (msr->samplecnt > UINT32_MAX) { - ms_log (2, "%s: Too many samples in input record (%" PRId64 " for a single record)\n", + ms_log (2, "%s: Too many samples in input record (%" PRId64 ") for a single record)\n", msr->sid, msr->samplecnt); return -1; } @@ -609,6 +615,8 @@ int64_t packoffset; int64_t totalpackedsamples; + uint16_t blockette_1000_offset = 0; + uint16_t blockette_1001_offset = 0; uint32_t datalength; nstime_t nextstarttime; uint16_t year; @@ -616,7 +624,8 @@ uint8_t hour; uint8_t min; uint8_t sec; - uint32_t nsec; + uint16_t fsec; + int8_t usec_offset; if (!msr) { @@ -665,16 +674,22 @@ memset (rawrec, 0, MS2FSDH_LENGTH); /* Pack fixed header and extra headers, returned size is data offset */ - headerlen = msr3_pack_header2 (msr, rawrec, reclen, verbose); + headerlen = msr3_pack_header2_offsets (msr, rawrec, reclen, + &blockette_1000_offset, + &blockette_1001_offset, + verbose); - if (headerlen < 0) + if (headerlen < 0 || blockette_1000_offset == 0) + { + libmseed_memory.free (rawrec); return -1; + } /* Short cut: if there are no samples, record packing is complete */ if (msr->numsamples <= 0) { /* Set encoding to text for consistency and to reduce expectations */ - *pMS2B1000_ENCODING (rawrec + 48) = DE_TEXT; + *pMS2B1000_ENCODING (rawrec + blockette_1000_offset) = DE_TEXT; /* Set empty part of record to zeros */ memset (rawrec + headerlen, 0, reclen - headerlen); @@ -698,6 +713,7 @@ if (!samplesize) { ms_log (2, "%s: Unknown sample type '%c'\n", msr->sid, msr->sampletype); + libmseed_memory.free (rawrec); return -1; } @@ -807,13 +823,15 @@ if (totalpackedsamples >= msr->numsamples) break; - /* Update record start time for next record */ + /* Update encoded record start time for next record */ nextstarttime = ms_sampletime (msr->starttime, totalpackedsamples, msr->samprate); - if (ms_nstime2time (nextstarttime, &year, &day, &hour, &min, &sec, &nsec)) + nstime_t second_nextstarttime = nstime2fsec_usec_offset (nextstarttime, &fsec, &usec_offset); + + if (ms_nstime2time (second_nextstarttime, &year, &day, &hour, &min, &sec, NULL)) { ms_log (2, "%s: Cannot convert next record starttime: %" PRId64 "\n", - msr->sid, nextstarttime); + msr->sid, second_nextstarttime); libmseed_memory.free (encoded); libmseed_memory.free (rawrec); return -1; @@ -824,7 +842,12 @@ *pMS2FSDH_HOUR (rawrec) = hour; *pMS2FSDH_MIN (rawrec) = min; *pMS2FSDH_SEC (rawrec) = sec; - *pMS2FSDH_FSEC (rawrec) = HO2u ((nsec / 100000), swapflag); + *pMS2FSDH_FSEC (rawrec) = HO2u (fsec, swapflag); + + if (blockette_1001_offset) + { + *pMS2B1001_MICROSECOND (rawrec + blockette_1001_offset) = usec_offset; + } } if (verbose >= 2) @@ -839,6 +862,124 @@ } /* End of msr3_pack_mseed2() */ /**********************************************************************/ /** + * @brief Repack a parsed miniSEED record into a version 2 record. + * + * Pack the parsed header into a version 2 header and copy the raw + * encoded data from the original record. The original record must be + * available at the ::MS3Record.record pointer. + * + * The new record will be the same length as the original record and an + * error will be returned if the repacked record would not fit. + * If the new record is shorter than the original record, the extra space + * will be zeroed. + * + * This can be used to efficiently convert format versions or modify + * header values without unpacking the data samples. + * + * @param[in] msr ::MS3Record containing record to repack + * @param[out] record Destination buffer for repacked record + * @param[in] recbuflen Length of destination buffer + * @param[in] verbose Controls logging verbosity, 0 is no diagnostic output + * + * @returns record length on success and -1 on error. + * + * \ref MessageOnError - this function logs a message on error + ***************************************************************************/ +int +msr3_repack_mseed2 (const MS3Record *msr, char *record, uint32_t recbuflen, + int8_t verbose) +{ + int headerlen; + uint16_t dataoffset; + uint32_t origdataoffset; + uint32_t origdatasize; + uint32_t totalsize; + uint8_t swapflag; + + if (!msr || !msr->record || !record) + { + ms_log (2, "%s(): Required input not defined: 'msr', 'msr->record', or 'record'\n", + __func__); + return -1; + } + + if ((int64_t)recbuflen < msr->reclen) + { + ms_log (2, "%s: Record buffer length (%u) is not large enough for requested record length (%d)\n", + msr->sid, recbuflen, msr->reclen); + return -1; + } + + if (msr->samplecnt > UINT16_MAX) + { + ms_log (2, "%s: Too many samples in input record (%" PRId64 ") for a single v2 record)\n", + msr->sid, msr->samplecnt); + return -1; + } + + /* Pack fixed header and blockettes */ + headerlen = msr3_pack_header2 (msr, record, recbuflen, verbose); + + if (headerlen < 0) + { + ms_log (2, "%s: Cannot pack miniSEED version 2 header\n", msr->sid); + return -1; + } + + /* Determine encoded data size */ + if (msr3_data_bounds (msr, &origdataoffset, &origdatasize)) + { + ms_log (2, "%s: Cannot determine original data bounds\n", msr->sid); + return -1; + } + + /* Determine offset to encoded data */ + if (msr->encoding == DE_STEIM1 || msr->encoding == DE_STEIM2) + { + dataoffset = 64; + while (dataoffset < headerlen) + dataoffset += 64; + + /* Zero memory between blockettes and data if any */ + memset (record + headerlen, 0, dataoffset - headerlen); + } + else + { + dataoffset = headerlen; + } + + totalsize = dataoffset + origdatasize; + + if (recbuflen < totalsize) + { + ms_log (2, "%s: Repacked minimum record length (%u) is larger than destination record buffer (%u)\n", + msr->sid, totalsize, recbuflen); + return -1; + } + + /* Copy encoded data into record */ + memcpy (record + dataoffset, msr->record + origdataoffset, origdatasize); + + /* Check if byte swapping is needed, miniSEED 2 is written big endian */ + swapflag = (ms_bigendianhost ()) ? 0 : 1; + + /* Update number of samples and data offset */ + *pMS2FSDH_NUMSAMPLES (record) = HO2u ((uint16_t)msr->samplecnt, swapflag); + *pMS2FSDH_DATAOFFSET (record) = HO2u (dataoffset, swapflag); + + /* Zero any space between encoded data and end of record */ + int32_t content = dataoffset + origdatasize; + if (content < msr->reclen) + memset (record + content, 0, msr->reclen - content); + + if (verbose >= 1) + ms_log (0, "%s: Repacked %" PRId64 " samples into a %u byte record\n", + msr->sid, msr->samplecnt, msr->reclen); + + return msr->reclen; +} /* End of msr3_repack_mseed2() */ + +/**********************************************************************/ /** * @brief Pack a miniSEED version 2 header into the specified buffer. * * Default values are: record length = 4096, encoding = 11 (Steim2). @@ -857,6 +998,34 @@ int msr3_pack_header2 (const MS3Record *msr, char *record, uint32_t recbuflen, int8_t verbose) { + return msr3_pack_header2_offsets (msr, record, recbuflen, NULL, NULL, verbose); +} + +/**********************************************************************/ /** + * @internal + * Pack a miniSEED version 2 header into the specified buffer and return the + * offsets of the 1000 and 1001 blockettes. + * + * Default values are: record length = 4096, encoding = 11 (Steim2). + * The defaults are triggered when \a msr.reclen and \a msr.encoding + * are set to -1. + * + * @param[in] msr ::MS3Record to pack + * @param[out] record Destination for packed header + * @param[in] recbuflen Length of destination buffer + * @param[out] blockette_1000_offset Byte offset to B1000, 0 if none + * @param[out] blockette_1001_offset Byte offset to B1001, 0 if none + * @param[in] verbose Controls logging verbosity, 0 is no diagnostic output + * + * @returns the size of the header (fixed and blockettes) on success, otherwise -1. + * + * \ref MessageOnError - this function logs a message on error + ***************************************************************************/ +static int +msr3_pack_header2_offsets (const MS3Record *msr, char *record, uint32_t recbuflen, + uint16_t *blockette_1000_offset, uint16_t *blockette_1001_offset, + int8_t verbose) +{ int written = 0; int8_t swapflag; uint32_t reclen; @@ -872,9 +1041,8 @@ uint8_t hour; uint8_t min; uint8_t sec; - uint32_t nsec; uint16_t fsec; - int8_t msec_offset; + int8_t usec_offset; uint32_t reclenexp = 0; uint32_t reclenfind; @@ -904,6 +1072,12 @@ return -1; } + /* Initialize blockette offsets to 0 */ + if (blockette_1000_offset) + *blockette_1000_offset = 0; + if (blockette_1001_offset) + *blockette_1001_offset = 0; + /* Use default record length and encoding if needed */ reclen = (msr->reclen < 0) ? MS_PACK_DEFAULT_RECLEN : msr->reclen; encoding = (msr->encoding < 0) ? MS_PACK_DEFAULT_ENCODING : msr->encoding; @@ -953,17 +1127,16 @@ if (verbose > 2 && swapflag) ms_log (0, "%s: Byte swapping needed for packing of header\n", msr->sid); + /* Calculate time at fractional 100usec resolution and microsecond offset */ + nstime_t second_nstime = nstime2fsec_usec_offset (msr->starttime, &fsec, &usec_offset); + /* Break down start time into individual components */ - if (ms_nstime2time (msr->starttime, &year, &day, &hour, &min, &sec, &nsec)) + if (ms_nstime2time (second_nstime, &year, &day, &hour, &min, &sec, NULL)) { ms_log (2, "%s: Cannot convert starttime: %" PRId64 "\n", msr->sid, msr->starttime); return -1; } - /* Calculate time at fractional 100usec resolution and microsecond offset */ - fsec = nsec / 100000; - msec_offset = ((nsec / 1000) - (fsec * 100)); - /* Generate factor & multipler representation of sample rate */ if (ms_genfactmult (msr3_sampratehz(msr), &factor, &multiplier)) { @@ -1136,10 +1309,13 @@ *pMS2B1000_RECLEN (record + written) = reclenexp; *pMS2B1000_RESERVED (record + written) = 0; + if (blockette_1000_offset) + *blockette_1000_offset = written; + written += 8; /* Add Blockette 1001 if microsecond offset or timing quality is present */ - if (yyjson_ptr_get_uint (ehroot, "/FDSN/Time/Quality", &header_uint) || msec_offset) + if (yyjson_ptr_get_uint (ehroot, "/FDSN/Time/Quality", &header_uint) || usec_offset) { *next_blockette = HO2u ((uint16_t)written, swapflag); next_blockette = pMS2B1001_NEXT (record + written); @@ -1153,10 +1329,13 @@ else *pMS2B1001_TIMINGQUALITY (record + written) = 0; - *pMS2B1001_MICROSECOND (record + written) = msec_offset; + *pMS2B1001_MICROSECOND (record + written) = usec_offset; *pMS2B1001_RESERVED (record + written) = 0; *pMS2B1001_FRAMECOUNT (record + written) = 0; + if (blockette_1001_offset) + *blockette_1001_offset = written; + written += 8; } @@ -1208,26 +1387,19 @@ if (yyjson_ptr_get_str (ehiterval, "/Time", &header_string)) { - int64_t l_nsec; - uint16_t l_fsec; - int8_t l_msec_offset; - - l_nsec = ms_timestr2btime (header_string, - (uint8_t *)pMS2B500_YEAR (record + written), - msr->sid, swapflag); + int8_t l_usec_offset; + nstime_t l_nstime = ms_timestr2btime (header_string, + (uint8_t *)pMS2B500_YEAR (record + written), + &l_usec_offset, msr->sid, swapflag); - if (l_nsec == -1) + if (l_nstime == NSTERROR) { ms_log (2, "%s: Cannot convert B500 time: %s\n", msr->sid, header_string); yyjson_doc_free (ehdoc); return -1; } - /* Calculate time at fractional 100usec resolution and microsecond offset */ - l_fsec = (uint16_t)(l_nsec / 100000); - l_msec_offset = (int8_t)((l_nsec / 1000) - (l_fsec * 100)); - - *pMS2B500_MICROSECOND (record + written) = l_msec_offset; + *pMS2B500_MICROSECOND (record + written) = l_usec_offset; } if (yyjson_ptr_get_uint (ehiterval, "/ReceptionQuality", &header_uint) && header_uint <= UINT8_MAX) @@ -1316,7 +1488,7 @@ if (yyjson_ptr_get_str (ehiterval, "/OnsetTime", &header_string)) { if (ms_timestr2btime (header_string, (uint8_t *)pMS2B200_YEAR (record + written), - msr->sid, swapflag) == -1) + NULL, msr->sid, swapflag) == NSTERROR) { ms_log (2, "%s: Cannot convert B%u time: %s\n", msr->sid, blockette_type, header_string); yyjson_doc_free (ehdoc); @@ -1435,7 +1607,7 @@ if (yyjson_ptr_get_str (ehiterval, "/BeginTime", &header_string)) { if (ms_timestr2btime (header_string, (uint8_t *)pMS2B300_YEAR (record + written), - msr->sid, swapflag) == -1) + NULL, msr->sid, swapflag) == NSTERROR) { ms_log (2, "%s: Cannot convert B%u time: %s\n", msr->sid, blockette_type, header_string); yyjson_doc_free (ehdoc); @@ -1600,7 +1772,7 @@ *pMS2B395_NEXT (record + written) = 0; if (ms_timestr2btime (header_string, (uint8_t *)pMS2B395_YEAR (record + written), - msr->sid, swapflag) == -1) + NULL, msr->sid, swapflag) == NSTERROR) { ms_log (2, "%s: Cannot convert B%u time: %s\n", msr->sid, blockette_type, header_string); yyjson_doc_free (ehdoc); @@ -2090,8 +2262,8 @@ } /* End of ms_genfactmult() */ /*************************************************************************** - * Convenience function to convert a month-day time string to a SEED - * 2.x "BTIME" structure. + * Convenience function to convert a time string to a SEED 2.x "BTIME" + * structure, and a microsecond offset. * * The 10-byte BTIME structure layout: * @@ -2104,33 +2276,41 @@ * unused uint8_t 7 Unused, included for alignment * fract uint16_t 8 0.0001 seconds, i.e. 1/10ths of milliseconds (0—9999) * - * Return nanoseconds on success and -1 on error. + * Return second-resolution nstime_t value on success and NSTERROR on error. * * \ref MessageOnError - this function logs a message on error ***************************************************************************/ -static inline int64_t -ms_timestr2btime (const char *timestr, uint8_t *btime, const char *sid, int8_t swapflag) +static inline nstime_t +ms_timestr2btime (const char *timestr, uint8_t *btime, int8_t *usec_offset, + const char *sid, int8_t swapflag) { uint16_t year; uint16_t day; uint8_t hour; uint8_t min; uint8_t sec; - uint32_t nsec; + uint16_t fsec; + int8_t l_usec_offset; nstime_t nstime; + nstime_t second_nstime; if (!timestr || !btime) { ms_log (2, "%s(%s): Required input not defined: 'timestr' or 'btime'\n", sid, __func__); - return -1; + return NSTERROR; } if ((nstime = ms_timestr2nstime (timestr)) == NSTERROR) - return -1; + return NSTERROR; - if (ms_nstime2time (nstime, &year, &day, &hour, &min, &sec, &nsec)) - return -1; + if (ms_nstime2time (nstime, &year, &day, &hour, &min, &sec, NULL)) + return NSTERROR; + + second_nstime = nstime2fsec_usec_offset (nstime, &fsec, &l_usec_offset); + + if (second_nstime == NSTERROR) + return NSTERROR; *((uint16_t *)(btime)) = HO2u (year, swapflag); *((uint16_t *)(btime + 2)) = HO2u (day, swapflag); @@ -2138,7 +2318,73 @@ *((uint8_t *)(btime + 5)) = min; *((uint8_t *)(btime + 6)) = sec; *((uint8_t *)(btime + 7)) = 0; - *((uint16_t *)(btime + 8)) = HO2u (nsec / 100000, swapflag); + *((uint16_t *)(btime + 8)) = HO2u (fsec, swapflag); - return nsec; + if (usec_offset) + *usec_offset = l_usec_offset; + + return second_nstime; } /* End of timestr2btime() */ + +/*************************************************************************** + * nstime2fsec_usec_offset + * + * Convert a nstime_t value to a time value in tenths of milliseconds (fsec) and + * a microsecond offset (usec_offset) from the fsec value. + * + * The nstime_t value returned is the nstime_t value at second resolution and + * the appropriate value to be combined with fsec and usec_offset to recover the + * time value (in microseconds). Nanosecond resolution is lost in this case. + * + * When converting nstime_t values with sub-microsecond resolution, the result + * will be rounded to the nearest microsecond to retain as much accuracy as + * possible. + * + * The tenths of milliseconds value will be rounded to the nearest value having + * a microsecond offset value between -50 to +49. + * + * Returns second-resolution nstime_t value on success and NSTERROR on error. + ***************************************************************************/ +static nstime_t +nstime2fsec_usec_offset (nstime_t nstime, uint16_t *fsec, int8_t *usec_offset) +{ + if (fsec == NULL || usec_offset == NULL) + return NSTERROR; + + /* Round to nearest microsecond (loses nanosecond precision) */ + nstime_t usec_time = (nstime + (nstime >= 0 ? 500 : -500)) / 1000 * 1000; + + /* Convert to microseconds and tenths of milliseconds */ + int64_t total_usec = usec_time / 1000; + int64_t total_fsec = usec_time / 100000; + + /* Calculate microsecond offset from tenths of milliseconds */ + *usec_offset = (int8_t)(total_usec - total_fsec * 100); + + /* Adjust to keep usec_offset in range [-50, +49] */ + if (*usec_offset > 49) + { + total_fsec += 1; + *usec_offset -= 100; + } + else if (*usec_offset < -50) + { + total_fsec -= 1; + *usec_offset += 100; + } + + /* Extract fsec within current second (0-9999) */ + int64_t fsec_remainder = total_fsec % 10000; + int64_t seconds = total_fsec / 10000; + + /* Handle negative modulo properly */ + if (fsec_remainder < 0) + { + fsec_remainder += 10000; + seconds -= 1; + } + *fsec = (uint16_t)fsec_remainder; + + /* Return second-resolution time */ + return seconds * NSTMODULUS; +} /* End of nstime2fsec_usec_offset() */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/test/Makefile new/libmseed-3.1.6/test/Makefile --- old/libmseed-3.1.5/test/Makefile 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/test/Makefile 2025-07-27 02:05:46.000000000 +0200 @@ -8,6 +8,14 @@ # CC : Specify the C compiler to use # CFLAGS : Specify compiler options to use +# Automatically configure URL support if libcurl is present +# Test for curl-config command and add build options if so +ifneq (,$(shell command -v curl-config)) + export LM_CURL_VERSION=$(shell curl-config --version) + export CFLAGS:=$(CFLAGS) -DLIBMSEED_URL + export LDFLAGS:=$(LDFLAGS) $(shell curl-config --libs) +endif + # Required compiler parameters CFLAGS += -I.. -I. Binary files old/libmseed-3.1.5/test/data/reference-testdata-nsec.mseed2 and new/libmseed-3.1.6/test/data/reference-testdata-nsec.mseed2 differ Binary files old/libmseed-3.1.5/test/data/reference-testdata-nsec.mseed3 and new/libmseed-3.1.6/test/data/reference-testdata-nsec.mseed3 differ Binary files old/libmseed-3.1.5/test/data/reference-testdata-olden.mseed2 and new/libmseed-3.1.6/test/data/reference-testdata-olden.mseed2 differ Binary files old/libmseed-3.1.5/test/data/reference-testdata-olden.mseed3 and new/libmseed-3.1.6/test/data/reference-testdata-olden.mseed3 differ Binary files old/libmseed-3.1.5/test/data/reference-testdata-repack.mseed2 and new/libmseed-3.1.6/test/data/reference-testdata-repack.mseed2 differ Binary files old/libmseed-3.1.5/test/data/reference-testdata-repack.mseed3 and new/libmseed-3.1.6/test/data/reference-testdata-repack.mseed3 differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/test/test-repack.c new/libmseed-3.1.6/test/test-repack.c --- old/libmseed-3.1.5/test/test-repack.c 1970-01-01 01:00:00.000000000 +0100 +++ new/libmseed-3.1.6/test/test-repack.c 2025-07-27 02:05:46.000000000 +0200 @@ -0,0 +1,100 @@ +#include <libmseed.h> +#include <tau/tau.h> + +#include "testdata.h" + +extern int cmpfiles (char *fileA, char *fileB); + +/* Write test output files. Reference files are at "data/reference-<name>" */ +#define TESTFILE_REPACK_V3 "testdata-repack.mseed3" +#define TESTFILE_REPACK_V2 "testdata-repack.mseed2" + +#define V2INPUT_RECORD "data/reference-testdata-defaults.mseed2" + +TEST (repack, v3) +{ + MS3Record *msr = NULL; + char buffer[8192]; + uint32_t flags; + int packedlength; + int rv; + + /* Read v2 input data */ + flags = MSF_UNPACKDATA; + rv = ms3_readmsr (&msr, V2INPUT_RECORD, flags, 0); + + CHECK (rv == MS_NOERROR, "ms3_readmsr() did not return expected MS_NOERROR"); + REQUIRE (msr != NULL, "ms3_readmsr() did not populate 'msr'"); + + /* Change some header fields */ + strcpy (msr->sid, "FDSN:XX_REPAK__H_H_Z"); + msr->starttime = ms_timestr2nstime ("2008-05-12T13:44:55.123456789Z"); + msr->samprate = 100.0; + msr->pubversion = 2; + + /* Repack to v3 record */ + packedlength = msr3_repack_mseed3 (msr, buffer, sizeof (buffer), 0); + + CHECK (packedlength > 0, "msr3_repack_mseed3() returned an error"); + + /* Write output */ + FILE *fd = fopen (TESTFILE_REPACK_V3, "wb"); + + CHECK (fd != NULL, "Failed to open output file"); + + rv = fwrite (buffer, 1, packedlength, fd); + + CHECK (rv == packedlength, "Failed to write output file"); + CHECK (fclose (fd) == 0, "Failed to close output file"); + + /* Compare to reference */ + rv = cmpfiles (TESTFILE_REPACK_V3, "data/reference-" TESTFILE_REPACK_V3); + + CHECK (rv == 0, "Repacked v3 record does not match reference"); + + ms3_readmsr(&msr, NULL, flags, 0); +} + +TEST (repack, v2) +{ + MS3Record *msr = NULL; + char buffer[8192]; + uint32_t flags; + int packedlength; + int rv; + + /* Read v2 input data */ + flags = MSF_UNPACKDATA; + rv = ms3_readmsr (&msr, V2INPUT_RECORD, flags, 0); + + CHECK (rv == MS_NOERROR, "ms3_readmsr() did not return expected MS_NOERROR"); + REQUIRE (msr != NULL, "ms3_readmsr() did not populate 'msr'"); + + /* Change some header fields */ + strcpy (msr->sid, "FDSN:XX_REPAK__H_H_Z"); + msr->starttime = ms_timestr2nstime ("2008-05-12T13:44:55.123456789Z"); + msr->samprate = 100.0; + msr->pubversion = 2; + + /* Repack to v2 record */ + packedlength = msr3_repack_mseed2 (msr, buffer, sizeof (buffer), 0); + + CHECK (packedlength > 0, "msr3_repack_mseed2() returned an error"); + + /* Write output */ + FILE *fd = fopen (TESTFILE_REPACK_V2, "wb"); + + CHECK (fd != NULL, "Failed to open output file"); + + rv = fwrite (buffer, 1, packedlength, fd); + + CHECK (rv == packedlength, "Failed to write output file"); + CHECK (fclose (fd) == 0, "Failed to close output file"); + + /* Compare to reference */ + rv = cmpfiles (TESTFILE_REPACK_V2, "data/reference-" TESTFILE_REPACK_V2); + + CHECK (rv == 0, "Repacked v2 record does not match reference"); + + ms3_readmsr(&msr, NULL, flags, 0); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libmseed-3.1.5/test/test-write.c new/libmseed-3.1.6/test/test-write.c --- old/libmseed-3.1.5/test/test-write.c 2025-04-24 14:08:48.000000000 +0200 +++ new/libmseed-3.1.6/test/test-write.c 2025-07-27 02:05:46.000000000 +0200 @@ -14,6 +14,8 @@ #define TESTFILE_STEIM1_V2 "testdata-steim1.mseed2" #define TESTFILE_STEIM2_V2 "testdata-steim2.mseed2" #define TESTFILE_DEFAULTS_V2 "testdata-defaults.mseed2" +#define TESTFILE_NSEC_V2 "testdata-nsec.mseed2" +#define TESTFILE_OLDEN_V2 "testdata-olden.mseed2" #define TESTFILE_TEXT_V3 "testdata-text.mseed3" #define TESTFILE_FLOAT32_V3 "testdata-float32.mseed3" @@ -23,6 +25,8 @@ #define TESTFILE_STEIM1_V3 "testdata-steim1.mseed3" #define TESTFILE_STEIM2_V3 "testdata-steim2.mseed3" #define TESTFILE_DEFAULTS_V3 "testdata-defaults.mseed3" +#define TESTFILE_NSEC_V3 "testdata-nsec.mseed3" +#define TESTFILE_OLDEN_V3 "testdata-olden.mseed3" TEST (write, msr) { @@ -134,8 +138,62 @@ REQUIRE (rv > 0, "msr3_writemseed() return unexpected value"); CHECK (!cmpfiles (TESTFILE_DEFAULTS_V3, "data/reference-" TESTFILE_DEFAULTS_V3), "Default encoding/reclen write mismatch"); + /* Nanosecond time resolution with Int32 data and a timing exception and 4096 max record length */ + msr->starttime = ms_timestr2nstime ("2012-05-12T00:00:00.123456789Z"); + msr->encoding = DE_INT32; + msr->reclen = 4096; + msr->numsamples = SINE_DATA_SAMPLES; + msr->datasamples = sinedata; + msr->sampletype = 'i'; + msr->extra = "{\"FDSN\":{" + "\"Time\":{" + "\"Exception\":[{" + "\"Time\":\"2012-05-12T00:00:26.987654321Z\"," + "\"VCOCorrection\":50.7080078125," + "\"ReceptionQuality\":100," + "\"Count\":7654," + "\"Type\":\"Valid\"," + "\"ClockStatus\":\"Drift=-1973usec, Satellite SNR in dB=23, 0, 26, 25, 29, 28\"" + "}]}," + "\"Clock\":{" + "\"Model\":\"Acme Corporation GPS3\"" + "}}}"; + msr->extralength = strlen (msr->extra); + + rv = msr3_writemseed (msr, TESTFILE_NSEC_V3, 1, flags, 0); + REQUIRE (rv > 0, "msr3_writemseed() return unexpected value"); + CHECK (!cmpfiles (TESTFILE_NSEC_V3, "data/reference-" TESTFILE_NSEC_V3), "Nanosecond timing write mismatch"); + + /* Old, pre-epoch times with Int32 data and a timing exception and 4096 max record length */ + msr->starttime = ms_timestr2nstime ("1964-03-27T21:11:24.987654321Z"); + msr->encoding = DE_INT32; + msr->reclen = 4096; + msr->numsamples = SINE_DATA_SAMPLES; + msr->datasamples = sinedata; + msr->sampletype = 'i'; + msr->extra = "{\"FDSN\":{" + "\"Time\":{" + "\"Exception\":[{" + "\"Time\":\"1964-03-27T21:11:48.123456789Z\"," + "\"Count\":1," + "\"Type\":\"Unexpected\"," + "\"ClockStatus\":\"Clock tower destroyed\"" + "}]}," + "\"Clock\":{" + "\"Model\":\"Ye Olde Clock Tower Company\"" + "}}}"; + msr->extralength = strlen (msr->extra); + + rv = msr3_writemseed (msr, TESTFILE_OLDEN_V3, 1, flags, 0); + REQUIRE (rv > 0, "msr3_writemseed() return unexpected value"); + CHECK (!cmpfiles (TESTFILE_OLDEN_V3, "data/reference-" TESTFILE_OLDEN_V3), "Old, pre-epoch times write mismatch"); + + msr->extra = NULL; + msr->extralength = 0; + /* Set miniSEED v2 flag */ flags |= MSF_PACKVER2; + msr->starttime = ms_timestr2nstime ("2012-05-12T00:00:00"); msr->reclen = 512; /* Text encoding */ @@ -224,6 +282,58 @@ REQUIRE (rv > 0, "msr3_writemseed() return unexpected value"); CHECK (!cmpfiles (TESTFILE_DEFAULTS_V2, "data/reference-" TESTFILE_DEFAULTS_V2), "Default encoding/reclen write mismatch"); + /* Nanosecond time resolution with Int32 data and a timing exception and 4096 record length */ + msr->starttime = ms_timestr2nstime ("2012-05-12T00:00:00.123456789Z"); + msr->encoding = DE_INT32; + msr->reclen = 4096; + msr->numsamples = SINE_DATA_SAMPLES; + msr->datasamples = sinedata; + msr->sampletype = 'i'; + msr->extra = "{\"FDSN\":{" + "\"Time\":{" + "\"Exception\":[{" + "\"Time\":\"2012-05-12T00:00:26.987654321Z\"," + "\"VCOCorrection\":50.7080078125," + "\"ReceptionQuality\":100," + "\"Count\":7654," + "\"Type\":\"Valid\"," + "\"ClockStatus\":\"Drift=-1973usec, Satellite SNR in dB=23, 0, 26, 25, 29, 28\"" + "}]}," + "\"Clock\":{" + "\"Model\":\"Acme Corporation GPS3\"" + "}}}"; + msr->extralength = strlen (msr->extra); + + rv = msr3_writemseed (msr, TESTFILE_NSEC_V2, 1, flags, 0); + REQUIRE (rv > 0, "msr3_writemseed() return unexpected value"); + CHECK (!cmpfiles (TESTFILE_NSEC_V2, "data/reference-" TESTFILE_NSEC_V2), "Nanosecond timing write mismatch"); + + /* Old, pre-epoch times with Int32 data and a timing exception and 4096 record length */ + msr->starttime = ms_timestr2nstime ("1964-03-27T21:11:24.987654321Z"); + msr->encoding = DE_INT32; + msr->reclen = 4096; + msr->numsamples = SINE_DATA_SAMPLES; + msr->datasamples = sinedata; + msr->sampletype = 'i'; + msr->extra = "{\"FDSN\":{" + "\"Time\":{" + "\"Exception\":[{" + "\"Time\":\"1964-03-27T21:11:48.123456789Z\"," + "\"Count\":1," + "\"Type\":\"Unexpected\"," + "\"ClockStatus\":\"Clock tower destroyed\"" + "}]}," + "\"Clock\":{" + "\"Model\":\"Ye Olde Clock Tower Company\"" + "}}}"; + msr->extralength = strlen (msr->extra); + + rv = msr3_writemseed (msr, TESTFILE_OLDEN_V2, 1, flags, 0); + REQUIRE (rv > 0, "msr3_writemseed() return unexpected value"); + CHECK (!cmpfiles (TESTFILE_OLDEN_V2, "data/reference-" TESTFILE_OLDEN_V2), "Old, pre-epoch times write mismatch"); + + msr->extra = NULL; + msr->extralength = 0; msr->datasamples = NULL; msr3_free (&msr); }