diff --git a/app/ann2rr.c b/app/ann2rr.c index 46815ec..0637c44 100644 --- a/app/ann2rr.c +++ b/app/ann2rr.c @@ -37,12 +37,19 @@ extern void exit(); char *pname; +/* wfdb-x: returns 1 if output format c (for -i, -v, or -V) is a unit of + time, which needs the sampling frequency. */ +static int time_format_needs_freq(int c) +{ + return (c == 'h' || c == 'm' || c == 's' || c == 't' || c == 'T'); +} + main(argc, argv) int argc; char *argv[]; { char *record = NULL, rrfstr[16], t0fstr[16], t1fstr[16], *prog_name(); - double sps, spm, sph; + double Ffreq = 0., sps, spm, sph; int Aflag = 0, a0, a0flag = 0, a1, a1flag = 0, cflag = 0, i, j, pflag = 0, previous_annot_valid = 0, rrdigits = 3, rrformat = 0, t0digits = 3, t0flag = 0, t0format = 0, t1digits = 3, @@ -54,6 +61,10 @@ char *argv[]; static WFDB_Time from, to, rr, t0, t1; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); /* Interpret command-line options. */ @@ -82,6 +93,17 @@ char *argv[]; } from = i; /* to be converted to sample intervals below */ break; + case 'F': /* sampling frequency follows (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } + break; case 'h': /* print usage summary and quit */ help(); exit(0); @@ -168,6 +190,7 @@ char *argv[]; a0flag = 1; break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -183,8 +206,26 @@ char *argv[]; exit(1); } - if ((sps = sampfreq(record)) < 0.) - (void)setsampfreq(sps = WFDB_DEFFREQ); + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz if + the record had no header); -F specifies one. It is needed for times + and intervals in units of time. Legacy mode: as in the C program. */ + if (wfdbx_get_legacy()) { + if ((sps = sampfreq(record)) < 0.) + (void)setsampfreq(sps = WFDB_DEFFREQ); + } + else if ((sps = sampfreq(record)) <= 0. && Ffreq > 0.) + (void)setsampfreq(sps = Ffreq); + if (!wfdbx_get_legacy() && sps <= 0. && + ((from && wfdbx_time_needs_freq(argv[(int)from])) || + (to && wfdbx_time_needs_freq(argv[(int)to])) || + time_format_needs_freq(rrformat) || + (t0flag && time_format_needs_freq(t0format)) || + (t1flag && time_format_needs_freq(t1format)))) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } spm = 60.0*sps; sph = 60.0*spm; @@ -328,6 +369,37 @@ char *s; } static char *help_strings[] = { + "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", + "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", + " -A print all intervals between annotations (default: print only RR", + " intervals; overrides -c, -p)", + " -c print intervals between consecutive valid annotations only", + " -f TIME start at specified TIME", + " -F FREQ sampling frequency, if the record header does not specify one", + " -h print this usage summary", + " -i FMT print intervals using format FMT (see below for values of FMT)", + " -p TYPE [TYPE ...] print intervals ending with annotations of specified", + " TYPEs only (use mnemonics such as N or V for TYPE)", + " -P TYPE [TYPE ...] print intervals beginning with specified types only", + " -t TIME stop at specified TIME", + " -v FMT print times of ends of intervals using format FMT (see below)", + " -V FMT print times of beginnings of intervals using format FMT (see below)", + " -w print annotations that end intervals", + " -W print annotations that begin intervals", + "By default, the output contains the RR intervals only, unless one or more", + "of -v, -V, -w, or -W are used. Intervals and times are printed in units of", + "sample intervals, unless a format is specified using -i, -v, or -V.", + "Formats can be 'h' (hours), 'm' (minutes), 's' (seconds), 't' (hh:mm:ss);", + "when used with -v or -V, format 'T' yields dates and times if available,", + "or the format can be omitted to obtain times in sample intervals. Formats", + "'h', 'm', and 's' may be followed by a number between 0 and 15, specifying", + "the number of decimal places (default: 3). For example, to obtain intervals", + "in seconds with 8 decimal places, use '-i s8'.", +NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", " -A print all intervals between annotations (default: print only RR", @@ -359,10 +431,11 @@ NULL void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) { - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) { + (void)fprintf(stderr, "%s\n", hs[i]); if (i % 23 == 0) { char b[5]; (void)fprintf(stderr, "--More--"); diff --git a/app/rdann.c b/app/rdann.c index b638a56..9c159b4 100644 --- a/app/rdann.c +++ b/app/rdann.c @@ -53,7 +53,7 @@ char *argv[]; { char *record = NULL, *prog_name(); signed char cflag = 0, chanmatch, nflag = 0, nummatch, sflag = 0, submatch; - double sps, tps, tpm, tph, n; + double Ffreq = 0., sps, tps, tpm, tph, n; int sprec, mprec, hprec; int eflag = 0, i, j, vflag = 0, xflag = 0; long afrom = 0L, anum = 0L, ato = 0L, bfrom = 0L, bnum = 0L, bto = 0L, @@ -65,6 +65,10 @@ char *argv[]; WFDB_Time sample_num; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); /* Accept old syntax. */ @@ -116,6 +120,17 @@ char *argv[]; } from = i; /* to be converted to sample intervals below */ break; + case 'F': /* sampling frequency follows (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } + break; case 'h': /* print usage summary and quit */ help(); exit(0); @@ -182,6 +197,7 @@ char *argv[]; eflag = 0; break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -197,8 +213,20 @@ char *argv[]; exit(1); } - if ((sps = sampfreq(record)) < 0.) - (void)setsampfreq(sps = WFDB_DEFFREQ); + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz if + the record had no header, as legacy mode does); -F specifies one. */ + if (wfdbx_get_legacy()) { + if ((sps = sampfreq(record)) < 0.) + (void)setsampfreq(sps = WFDB_DEFFREQ); + } + else if ((sps = sampfreq(record)) <= 0. && Ffreq > 0.) + (void)setsampfreq(sps = Ffreq); + if (!wfdbx_get_legacy() && sps <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } ai.stat = WFDB_READ; if (annopen(record, &ai, 1) < 0) /* open annotation file */ @@ -347,6 +375,26 @@ char *s; } static char *help_strings[] = { + "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", + "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", + " -c CHAN print annotations with specified CHAN only", + " -e * show annotation times as elapsed times", + " -f TIME start at specified TIME", + " -F FREQ sampling frequency, if the record header does not", + " specify one", + " -h print this usage summary", + " -n NUM print annotations with specified NUM only", + " -p TYPE [TYPE ...] print annotations of specified TYPEs only", + " -s SUBTYPE print annotations with specified SUBTYPE only", + " -t TIME stop at specified TIME", + " -v print column headings", + " -x * use alternate time format (seconds, minutes, hours)", + "* Only one of -e and -x can be used.", +NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", " -c CHAN print annotations with specified CHAN only", @@ -366,8 +414,9 @@ NULL void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) + (void)fprintf(stderr, "%s\n", hs[i]); } diff --git a/app/rdsamp.c b/app/rdsamp.c index 3833c3c..c3bf75e 100644 --- a/app/rdsamp.c +++ b/app/rdsamp.c @@ -60,6 +60,10 @@ char *argv[]; WFDB_Time from = 0L, maxl = 0L, to = 0L; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); for (i = 1 ; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { @@ -164,11 +168,24 @@ char *argv[]; } if ((nsig = isigopen(record, si, nsig)) <= 0) exit(2); - for (i = 0; i < nsig; i++) - if (si[i].gain == 0.0) si[i].gain = WFDB_DEFGAIN; + if (wfdbx_get_legacy()) /* wfdb-x: the C program's default gain */ + for (i = 0; i < nsig; i++) + if (si[i].gain == 0.0) si[i].gain = WFDB_DEFGAIN; if (highres) setgvmode(WFDB_HIGHRES); freq = sampfreq(NULL); + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz + if the header did not specify one, as legacy mode does). */ + if (!wfdbx_get_legacy() && freq <= 0. && + ((from > 0L && wfdbx_time_needs_freq(argv[from])) || + (to > 0L && wfdbx_time_needs_freq(argv[to])) || + (maxl > 0L && wfdbx_time_needs_freq(argv[maxl])) || + xflag || (pflag && (timeunits != SAMPLES || vflag)))) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + pname, record); + exit(2); + } if (from > 0L && (from = strtim(argv[from])) < 0L) from = -from; if (isigsettime(from) < 0) @@ -199,6 +216,17 @@ char *argv[]; sig[i] = i; } + /* wfdb-x: no default gain (the C program assumed 200 adu/mV for + uncalibrated signals, as legacy mode does). */ + if (pflag && !wfdbx_get_legacy()) + for (i = 0; i < nsig; i++) + if (si[sig[i]].gain == 0.0) { + (void)fprintf(stderr, + "%s: signal %d of record %s is uncalibrated\n", + pname, sig[i], record); + exit(2); + } + /* Reset 'from' if a search was requested. */ if (search && ((s = findsig(search)) < 0 || (from = tnextvec(s, from)) < 0)) { @@ -359,7 +387,8 @@ char *argv[]; for (i = 0; i < nsig; i++) { p = si[sig[i]].units; - if (p == NULL) p = "mV"; + if (p == NULL) /* wfdb-x: unspecified units are shown as "nd" (C, legacy mode: mV) */ + p = wfdbx_get_legacy() ? "mV" : "nd"; if (cflag == 0) { char ustring[16]; int len; diff --git a/app/rr2ann.c b/app/rr2ann.c index 4866393..dcfb4df 100644 --- a/app/rr2ann.c +++ b/app/rr2ann.c @@ -54,7 +54,7 @@ char *argv[]; { static WFDB_Anninfo ai; static WFDB_Annotation annot; - WFDB_Frequency sps = 0.0; + WFDB_Frequency f, sps = 0.0; double rr, t = 0.0, xfactor = 1.0; static char line[MAXLINELEN]; char annstr[10], *p, *record = NULL, *prog_name(); @@ -62,6 +62,10 @@ char *argv[]; void help(); extern double atof(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); /* Interpret command-line options. */ @@ -129,11 +133,22 @@ char *argv[]; /* sampfreq is likely to fail, since the header probably doesn't exist. Suppress its error message. */ wfdbquiet(); - if (sampfreq(record) <= 0.0) { + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz, + as legacy mode does); -F specifies one if the record header does + not. */ + if ((f = sampfreq(record)) <= 0.0) { wfdbverbose(); /* re-enable error reporting */ - if (sps == 0.0) sps = WFDB_DEFFREQ; + if (sps == 0.0 && wfdbx_get_legacy()) + sps = WFDB_DEFFREQ; + else if (sps == 0.0) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } setsampfreq(sps); - if (newheader(record) < 0) + if ((f < 0.0 || wfdbx_get_legacy()) && newheader(record) < 0) + /* no header: create one (the C program: in any case) */ exit(2); } else { @@ -194,6 +209,30 @@ char *s; } static char *help_strings[] = { + "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...] = argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } + break; case 'h': /* print usage summary and quit */ help(); exit(0); @@ -145,6 +164,7 @@ char *argv[]; to = i; break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -176,8 +196,23 @@ char *argv[]; recursively if something goes wrong. */ putenv("WFDBANNSORT=0"); - if ((sps = sampfreq(record)) < 0.) - (void)setsampfreq(sps = WFDB_DEFFREQ); + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz if + the record had no header); -F specifies one. It is needed only to + convert the -f and -t times. Legacy mode: as in the C program. */ + if (wfdbx_get_legacy()) { + if ((sps = sampfreq(record)) < 0.) + (void)setsampfreq(sps = WFDB_DEFFREQ); + } + else if ((sps = sampfreq(record)) <= 0. && Ffreq > 0.) + (void)setsampfreq(sps = Ffreq); + if (!wfdbx_get_legacy() && sps <= 0. && + ((from && wfdbx_time_needs_freq(argv[(int)from])) || + (to && wfdbx_time_needs_freq(argv[(int)to])))) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } spm = 60.0*sps; ai[0].stat = WFDB_READ; @@ -197,8 +232,16 @@ char *argv[]; if ((tps = getiaorigfreq(0)) > 0) { setafreq(tps); setiafreq(0, tps); - from = from * tps / sps + 0.5; - to = to * tps / sps + 0.5; + if (wfdbx_get_legacy() || sps > 0.) { + from = from * tps / sps + 0.5; + to = to * tps / sps + 0.5; + } + else if (from || to) { /* wfdb-x: no default sampling frequency */ + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } } /* Build a linked list of annotations in memory. */ @@ -281,6 +324,14 @@ WFDB_Annotation *pa; well for nearly-ordered input. */ ap = lastp; while (ap) { + /* wfdb-x: the list head is not an annotation; the original program + compared it, and took an annotation at time 0 (num 0, chan 0) + for a duplicate of it (losing it). Legacy mode: as the + original. */ + if (ap == &annlist && !wfdbx_get_legacy()) { + ap = NULL; + break; + } if (pa->time > (ap->annotation).time || (pa->time == (ap->annotation).time && pa->num > (ap->annotation).num) || @@ -303,6 +354,11 @@ WFDB_Annotation *pa; if (newp->prev = ap->prev) (newp->prev)->next = newp; if (newp->next = ap->next) (newp->next)->prev = newp; else lastp = newp; + /* wfdb-x: if ap was the first entry but its prev link does not + lead to the head, the original left the head pointing to the + freed entry (undefined behavior); the head is updated. */ + if (annlist.next == ap && !wfdbx_get_legacy()) + annlist.next = newp; if ((ap->annotation).aux) free((ap->annotation).aux); if (ap != &annlist) free(ap); } @@ -353,6 +409,20 @@ char *s; } static char *help_strings[] = { + "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", + "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", + " -f TIME start at specified TIME", + " -F FREQ sampling frequency, if the record header does not specify one", + " -h print this usage summary", + " -o OUTANN write output as annotator OUTANN (required with -f or -t)", + " -t TIME stop at specified TIME", + "The sorted output replaces the input if -o is omitted, the input is in", + "the current directory, and the input is not already sorted.", +NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", " -f TIME start at specified TIME", @@ -367,8 +437,9 @@ NULL void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) + (void)fprintf(stderr, "%s\n", hs[i]); } diff --git a/app/time2sec.c b/app/time2sec.c index e4f3426..e363e65 100644 --- a/app/time2sec.c +++ b/app/time2sec.c @@ -34,6 +34,10 @@ char **argv; int milliseconds; long seconds; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + if (argc == 4 && strcmp(argv[1], "-r") == 0) { (void)isigopen(argv[2], NULL, 0); t = strtim(argv[3]); diff --git a/app/wfdbcat.c b/app/wfdbcat.c index 135ea7e..95364b0 100644 --- a/app/wfdbcat.c +++ b/app/wfdbcat.c @@ -45,6 +45,10 @@ char *argv[]; static char buf[BUFSIZE]; WFDB_FILE *ifile; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); if (argc < 2 || (argc == 2 && strcmp(argv[1], "-h") == 0)) { (void)fprintf(stderr, "usage: %s FILE [FILE ...]\n", pname); diff --git a/app/wfdbdesc.c b/app/wfdbdesc.c index bd0526b..18423ab 100644 --- a/app/wfdbdesc.c +++ b/app/wfdbdesc.c @@ -39,6 +39,11 @@ char *argv[]; FILE *ifile; WFDB_Siginfo *s; WFDB_Time t; + WFDB_Frequency freq; + + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); pname = prog_name(argv[0]); if (argc < 2) { @@ -82,10 +87,16 @@ char *argv[]; } while (info = getinfo((char *)NULL)); (void)printf("=====\n"); } - p = mstimstr(0L); + /* wfdb-x: if the sampling frequency is not specified, times are not + shown (the C program assumed 250 Hz). In legacy mode, the output is + that of the C program. */ + freq = sampfreq(NULL); + p = (freq > 0. || wfdbx_get_legacy()) ? mstimstr(0L) : ""; (void)printf("\nStarting time: %s\n", *p == '[' ? p : "not specified"); (void)printf("Length: "); - if (nsig < 1 || t > 0L) + if (freq <= 0. && !wfdbx_get_legacy() && (nsig < 1 || t > 0L)) + (void)printf("%"WFDB_Pd_TIME" sample intervals\n", t); + else if (nsig < 1 || t > 0L) (void)printf("%s (%"WFDB_Pd_TIME" sample intervals)\n", t > 0L ? mstimstr(t) : "0", t); else if (s[0].fmt && (ifile = fopen(s[0].fname, "rb")) && @@ -123,7 +134,10 @@ char *argv[]; t = -1; break; } - if (t >= 0) + if (t >= 0 && freq <= 0. && !wfdbx_get_legacy()) + printf("%"WFDB_Pd_TIME" sample intervals" + " [from signal file size]\n", t); + else if (t >= 0) printf("%s (%"WFDB_Pd_TIME" sample intervals)" " [from signal file size]\n", mstimstr(t), t); @@ -131,25 +145,39 @@ char *argv[]; printf("not specified\n"); } else (void)printf("not specified\n"); - (void)printf("Sampling frequency: %.12g Hz\n", sampfreq(NULL)); + if (freq > 0. || wfdbx_get_legacy()) + (void)printf("Sampling frequency: %.12g Hz\n", freq); + else + (void)printf("Sampling frequency: not specified\n"); (void)printf("%d signal%s\n", nsig, nsig == 1 ? "" : "s"); if (nsig < 1) exit(2); if (msrec && s[0].nsamp != 0L) { (void)printf("----------------------------------------------\n"); (void)printf("The following data apply to the first segment:\n"); (void)printf("----------------------------------------------\n"); - (void)printf("Segment length: %s (%ld sample intervals)\n", - mstimstr(s[0].nsamp), s[0].nsamp); + if (freq > 0. || wfdbx_get_legacy()) + (void)printf("Segment length: %s (%ld sample intervals)\n", + mstimstr(s[0].nsamp), s[0].nsamp); + else + (void)printf("Segment length: %ld sample intervals\n", + s[0].nsamp); } for (i = 0; i < nsig; i++) { (void)printf("Group %d, Signal %d:\n", s[i].group, i); (void)printf(" File: %s\n", s[i].fmt ? s[i].fname : "[none]"); (void)printf(" Description: %s\n", s[i].desc); (void)printf(" Gain: "); - if (s[i].gain == 0.) - (void)printf("uncalibrated; assume %g", WFDB_DEFGAIN); - else (void)printf("%.12g", s[i].gain); - (void)printf(" adu/%s\n", s[i].units ? s[i].units : "mV"); + if (wfdbx_get_legacy()) { /* wfdb-x: as in the C program */ + if (s[i].gain == 0.) + (void)printf("uncalibrated; assume %g", WFDB_DEFGAIN); + else (void)printf("%.12g", s[i].gain); + (void)printf(" adu/%s\n", s[i].units ? s[i].units : "mV"); + } + else if (s[i].gain == 0.) /* wfdb-x: no default gain */ + (void)printf("uncalibrated\n"); + else + (void)printf("%.12g adu/%s\n", s[i].gain, /* wfdb-x: unspecified units are shown as "nd" (C assumed mV) */ + s[i].units ? s[i].units : "nd"); (void)printf(" Initial value: %d\n", s[i].initval); if (s[i].fmt != 0) (void)printf(" Storage format: %d", s[i].fmt); @@ -160,7 +188,10 @@ char *argv[]; (void)printf(" I/O: "); if (s[i].bsize == 0) (void)printf("can be unbuffered\n"); else (void)printf("%d-byte blocks\n", s[i].bsize); - (void)printf(" ADC resolution: %d bits\n", s[i].adcres); + if (s[i].adcres > 0 || wfdbx_get_legacy()) + (void)printf(" ADC resolution: %d bits\n", s[i].adcres); + else /* wfdb-x: no default resolution */ + (void)printf(" ADC resolution: not specified\n"); (void)printf(" ADC zero: %d\n", s[i].adczero); (void)printf(" Baseline: %d\n", s[i].baseline); if (s[i].nsamp > 0L) diff --git a/app/wfdbtime.c b/app/wfdbtime.c index bb3ba3b..0aebdb3 100644 --- a/app/wfdbtime.c +++ b/app/wfdbtime.c @@ -33,12 +33,23 @@ main(int argc, char **argv) int i; WFDB_Time t; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + for (i = 1; i < argc; i++) { if (strcmp(argv[i], "-r") == 0) { if (record) wfdbquit(); if (++i == argc) break; record = argv[i]; if (isigopen(record, NULL, 0) < 0) exit(2); + if (!wfdbx_get_legacy() && sampfreq(NULL) <= 0.) { + /* wfdb-x: no default frequency (except in legacy mode) */ + fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + argv[0], record); + exit(2); + } } else if (record) { if ((t = strtim(argv[i])) < 0L) t = -t; diff --git a/app/wfdbwhich.c b/app/wfdbwhich.c index d492a3e..0882ea3 100644 --- a/app/wfdbwhich.c +++ b/app/wfdbwhich.c @@ -38,6 +38,10 @@ char *argv[]; char *filename, *pname, *prog_name(); int status = 0; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); if (argc == 2 && strcmp(argv[1], "-h") == 0) argc = 0; /* give usage instructions and quit */ diff --git a/app/wrann.c b/app/wrann.c index fe5dc45..9df9403 100644 --- a/app/wrann.c +++ b/app/wrann.c @@ -46,6 +46,10 @@ char *argv[]; WFDB_Time tm; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); /* Interpret command-line options. */ diff --git a/app/wrsamp.c b/app/wrsamp.c index 08ced06..78e2fd0 100644 --- a/app/wrsamp.c +++ b/app/wrsamp.c @@ -192,9 +192,10 @@ char *argv[]; { char **ap, *cp, **desc, *gain = "", *ifname = "(stdin)", *line = NULL, ofname[40], *p, *record = NULL, rsep = '\n', - *scale = "", sflag = 0, trim = 0, Xflag = 0, **units, *prog_name(); + *scale = "", sflag = 0, trim = 0, Xflag = 0, **units, *prog_name(), + *res = "", *uopt = NULL; /* wfdb-x: -R and -U */ static char btime[25], **dstrings, **ustrings; - double freq = WFDB_DEFFREQ, *scalef, v; + double freq = 0.0, *scalef, v; /* wfdb-x: no default frequency */ #ifndef atof double atof(); #endif @@ -209,7 +210,13 @@ char *argv[]; WFDB_Siginfo *si; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); + if (wfdbx_get_legacy()) + freq = WFDB_DEFFREQ; /* wfdb-x: the default of the C program */ for (i = 1; i < argc && *argv[i] == '-'; i++) { switch (*(argv[i]+1)) { @@ -246,6 +253,25 @@ char *argv[]; help(); exit(1); break; + case 'R': /* wfdb-x: ADC resolution(s) in bits */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc) { + (void)fprintf(stderr, + "%s: ADC resolution(s) must follow -R\n", pname); + exit(1); + } + res = argv[i]; + break; + case 'U': /* wfdb-x: units */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc) { + (void)fprintf(stderr, "%s: units must follow -U\n", pname); + exit(1); + } + uopt = argv[i]; + break; case 'i': if (++i >= argc) { (void)fprintf(stderr, "%s: input file name must follow -i\n", @@ -318,12 +344,37 @@ char *argv[]; zflag = 1; break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); } } + /* wfdb-x: the sampling frequency, the gains, the ADC resolutions, and the + units (of calibrated signals) must be specified when a header is + written (the C program assumed 250 Hz, 200 adu/mV, a resolution based + on the format, and millivolts), as legacy mode does. */ + if (record != NULL && !wfdbx_get_legacy()) { + if (freq <= 0.) { + (void)fprintf(stderr, + "%s: the sampling frequency must be specified (-F)\n", + pname); + exit(1); + } + if (*gain == '\0') { + (void)fprintf(stderr, "%s: the gain(s) must be specified (-G)\n", + pname); + exit(1); + } + if (*res == '\0') { + (void)fprintf(stderr, + "%s: the ADC resolution(s) must be specified (-R)\n", + pname); + exit(1); + } + } + nosig = argc - i; /* any remaining arguments are column numbers */ /* Determine the number of input columns, and (if present) the signal @@ -467,7 +518,22 @@ char *argv[]; (void)sprintf(tdesc, "col %d", fv[i]); SSTRCPY(si[i].desc, tdesc); } - if (ustrings) { + if (uopt) { /* wfdb-x: -U (the last one applies to the rest) */ + char *q; + + while (*uopt == ' ') + uopt++; + if (*uopt) { + for (q = uopt; *q && *q != ' '; q++) + ; + SALLOC(si[i].units, q - uopt + 1, 1); + strncpy(si[i].units, uopt, q - uopt); + uopt = q; + } + else /* no more units: those of the previous signal */ + si[i].units = (i > 0) ? si[i-1].units : ""; + } + else if (ustrings) { SSTRCPY(si[i].units, ustrings[fv[i]]); } else @@ -476,35 +542,64 @@ char *argv[]; si[i].fmt = format; si[i].spf = 1; si[i].bsize = 0; - switch (format) { - case 508: - case 80: si[i].adcres = 8; break; - case 310: - case 311: si[i].adcres = 10; break; - case 212: si[i].adcres = 12; break; - case 516: - case 16: - case 160: - case 61: si[i].adcres = 16; break; - case 524: - case 24: si[i].adcres = 24; break; - case 32: si[i].adcres = 32; break; - default: - /* resolution for unknown future formats */ - if (format > 100 && format % 100 > 0 && format % 100 <= 32) - si[i].adcres = format % 100; + /* wfdb-x: the ADC resolutions are given by -R (the last one applies + to the remaining signals); the C program (and legacy mode) inferred + them from the format or assumed WFDB_DEFRES. */ + if (wfdbx_get_legacy()) { + switch (format) { + case 508: + case 80: si[i].adcres = 8; break; + case 310: + case 311: si[i].adcres = 10; break; + case 212: si[i].adcres = 12; break; + case 516: + case 16: + case 160: + case 61: si[i].adcres = 16; break; + case 524: + case 24: si[i].adcres = 24; break; + case 32: si[i].adcres = 32; break; + default: + /* resolution for unknown future formats */ + if (format > 100 && format % 100 > 0 && format % 100 <= 32) + si[i].adcres = format % 100; + else + si[i].adcres = WFDB_DEFRES; + } + } + else { + while (*res == ' ') + res++; + if (sscanf(res, "%d", &(si[i].adcres)) < 1) + si[i].adcres = (i == 0) ? 0 : si[i-1].adcres; else - si[i].adcres = WFDB_DEFRES; + while (*res != '\0' && *res != ' ') + res++; + if (record != NULL && si[i].adcres < 1) { + (void)fprintf(stderr, + "%s: the ADC resolution(s) must be specified (-R)\n", + pname); + exit(1); + } } si[i].adczero = 0; si[i].baseline = 0; while (*gain == ' ') gain++; if (sscanf(gain, "%lf", &(si[i].gain)) < 1) - si[i].gain = (i == 0) ? WFDB_DEFGAIN : si[i-1].gain; + si[i].gain = (i == 0) ? (wfdbx_get_legacy() ? WFDB_DEFGAIN : 0.) : + si[i-1].gain; /* wfdb-x: C (legacy mode): WFDB_DEFGAIN */ else while (*gain != '\0' && *gain != ' ') gain++; + /* wfdb-x: the units of a calibrated signal must be specified, by -U + or by the input (the C program left them out: millivolts). */ + if (record != NULL && !wfdbx_get_legacy() && si[i].gain != 0. && + *si[i].units == '\0') { + (void)fprintf(stderr, "%s: the units must be specified (-U)\n", + pname); + exit(1); + } while (*scale == ' ') scale++; if (sscanf(scale, "%lf", &(scalef[i])) < 1) @@ -601,6 +696,54 @@ char *s; } static char *help_strings[] = { + "usage: %s [OPTIONS ...] [COLUMN ...]\n", + "where COLUMN selects a field to be copied (leftmost field is column 0),", + "and OPTIONS may include:", + " -c check that each input line contains the same number of fields", + " -d add dither to the input", + " -f N start copying with line N (default: 0)", + " -F FREQ specify frequency to be written to header file (required", + " with -o)", + " -G GAIN specify gain(s) to be written to header file (required with", + " -o; 0 means uncalibrated)", + " -h print this usage summary", + " -i FILE read input from FILE (default: standard input)", + " -o RECORD save output in RECORD.dat, and generate a header file for", + " RECORD (default: write to standard output in format 16, do", + " not generate a header file)", + " -O FORMAT save output in the specified FORMAT (default: 16)", + " -r RSEP interpret RSEP as the input line separator (default: linefeed)", + " -R BITS specify ADC resolution(s) in bits (required with -o)", + " -s FSEP interpret any character in FSEP as an input field separator", + " (default: space, tab, carriage-return, or comma) ", + " -t N stop copying at line N (default: end of input file)", + " -U UNITS specify units (required with -o for calibrated signals, unless", + " the input gives them)", + " -x SCALE multiply inputs by SCALE factor(s) (default: 1)", + " -z don't copy column 0 unless explicitly specified", + "", + "The input is a text file with a sample of each signal on each line. Samples", + "can be separated by tabs, spaces, commas, or any combination of these.", + "Consecutive separators are equivalent to single separators.", + "", + "To specify different GAIN or SCALE values for each output signal, provide", + "a quoted list of values, e.g., -G \"100 50\" defines the gain for signal 0", + "as 100, and the gain for signal 1 (and any additional signals) as 50.", + "", + "If the first input line contains any alphabetic characters, it is assumed", + "to contain column headings, which are copied as signal descriptions into", + "RECORD.hea. If the second input line also contains alphabetic characters,", + "it is assumed to specify the physical units for each column, which are", + "copied as units strings into RECORD.hea. The first line containing samples", + "is line 0.", + "", + "If no COLUMN is specified, all fields are copied in order; column 0 can be", + "omitted in this case by using the '-z' option.", +NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s [OPTIONS ...] [COLUMN ...]\n", "where COLUMN selects a field to be copied (leftmost field is column 0),", "and OPTIONS may include:", @@ -645,10 +788,11 @@ NULL void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) { - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) { + (void)fprintf(stderr, "%s\n", hs[i]); if (i % 23 == 0) { char b[5]; (void)fprintf(stderr, "--More--"); diff --git a/app/xform.c b/app/xform.c index b8ae037..c4d6261 100644 --- a/app/xform.c +++ b/app/xform.c @@ -67,6 +67,10 @@ char *argv[]; WFDB_Annotation annot; WFDB_Siginfo *dfin, *dfout; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); /* Interpret the command-line arguments. */ @@ -294,6 +298,15 @@ char *argv[]; else if (Hflag) setgvmode(WFDB_HIGHRES); ifreq = sampfreq(NULL); + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz if + the input header did not specify one, as legacy mode does). */ + if (ifreq <= 0. && !wfdbx_get_legacy()) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + pname, irec); + wfdbquit(); + exit(2); + } spf = getspf(); if (Mflag) { @@ -318,6 +331,14 @@ char *argv[]; /* Determine the output sampling frequency. */ if (orec) ofreq = sampfreq(orec); + if (ofreq == 0. && !wfdbx_get_legacy()) { + /* wfdb-x: no default sampling frequency */ + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + pname, orec); + wfdbquit(); + exit(2); + } (void)setsampfreq(ifreq/spf); if (ifreq != ofreq) { @@ -384,7 +405,15 @@ char *argv[]; script_fgets(answer, sizeof(answer), ttyin); (void)sscanf(answer, "%lf", &ofreq); } while (ofreq < 0); - if (ofreq == 0) ofreq = WFDB_DEFFREQ; + if (ofreq == 0 && wfdbx_get_legacy()) + ofreq = WFDB_DEFFREQ; /* wfdb-x: as in the C program */ + else if (ofreq == 0) { /* wfdb-x: the C program assumed 250 Hz */ + (void)fprintf(stderr, + "%s: the output sampling frequency must be > 0\n", + pname); + wfdbquit(); + exit(1); + } if (nosig > 0) { (void)fprintf(stderr, "Specify the name of the directory in which the output\n"); @@ -493,48 +522,89 @@ char *argv[]; dfout[i].units = *units[i] ? units[i] : NULL; } (void)fprintf(stderr, " Signal %d gain (adu/%s) [%g]: ", - i, dfout[i].units ? dfout[i].units : "mV", - dfout[i].gain); + i, dfout[i].units ? dfout[i].units : + (wfdbx_get_legacy() ? "mV" : "nd"), + dfout[i].gain); /* wfdb-x: C showed "mV" */ script_fgets(answer, sizeof(answer), ttyin); sscanf(answer, "%lf", &dfout[i].gain); - do { + /* wfdb-x: a resolution is offered only if it comes from the input + (or the previous output signal) and suits the output format; the + C program otherwise offered one based on the format (12 bits for + 16-bit formats). Without one, the user must give it. Legacy mode: + as in the C program. */ + if (wfdbx_get_legacy()) { + do { + if (i > 0) dfout[i].adcres = dfout[i-1].adcres; + switch (dfout[i].fmt) { + case 80: + case 508: + dfout[i].adcres = 8; + break; + case 212: + if (dfout[i].adcres < 8 || dfout[i].adcres > 12) + dfout[i].adcres = 12; + break; + case 310: + if (dfout[i].adcres < 8 || dfout[i].adcres > 10) + dfout[i].adcres = 10; + break; + case 24: + case 524: + if (dfout[i].adcres < 8 || dfout[i].adcres > 24) + dfout[i].adcres = 24; + break; + case 32: + if (dfout[i].adcres < 8 || dfout[i].adcres > 32) + dfout[i].adcres = 32; + break; + case 16: + case 61: + case 160: + case 516: + default: + if (dfout[i].adcres < 8 || dfout[i].adcres > 16) + dfout[i].adcres = WFDB_DEFRES; + break; + } + (void)fprintf(stderr, + " Signal %d ADC resolution in bits (8-32) [%d]: ", i, + dfout[i].adcres); + script_fgets(answer, sizeof(answer), ttyin); + (void)sscanf(answer, "%d", &dfout[i].adcres); + } while (dfout[i].adcres < 8 || dfout[i].adcres > 32); + } + else do { + int maxres; + if (i > 0) dfout[i].adcres = dfout[i-1].adcres; switch (dfout[i].fmt) { case 80: - case 508: - dfout[i].adcres = 8; - break; - case 212: - if (dfout[i].adcres < 8 || dfout[i].adcres > 12) - dfout[i].adcres = 12; - break; - case 310: - if (dfout[i].adcres < 8 || dfout[i].adcres > 10) - dfout[i].adcres = 10; - break; + case 508: maxres = 8; break; + case 212: maxres = 12; break; + case 310: maxres = 10; break; case 24: - case 524: - if (dfout[i].adcres < 8 || dfout[i].adcres > 24) - dfout[i].adcres = 24; - break; - case 32: - if (dfout[i].adcres < 8 || dfout[i].adcres > 32) - dfout[i].adcres = 32; - break; - case 16: - case 61: - case 160: - case 516: - default: - if (dfout[i].adcres < 8 || dfout[i].adcres > 16) - dfout[i].adcres = WFDB_DEFRES; - break; + case 524: maxres = 24; break; + case 32: maxres = 32; break; + default: maxres = 16; break; } - (void)fprintf(stderr, + if (dfout[i].adcres >= 8 && dfout[i].adcres <= maxres) + (void)fprintf(stderr, " Signal %d ADC resolution in bits (8-32) [%d]: ", i, - dfout[i].adcres); + dfout[i].adcres); + else { + (void)fprintf(stderr, + " Signal %d ADC resolution in bits (8-32): ", i); + dfout[i].adcres = 0; + } script_fgets(answer, sizeof(answer), ttyin); (void)sscanf(answer, "%d", &dfout[i].adcres); + if (dfout[i].adcres == 0) { + (void)fprintf(stderr, + "%s: the ADC resolution of signal %d must be specified\n", + pname, i); + wfdbquit(); + exit(1); + } } while (dfout[i].adcres < 8 || dfout[i].adcres > 32); (void)fprintf(stderr, " Signal %d ADC zero level (adu) [%d]: ", i, dfout[i].adczero); @@ -656,7 +726,18 @@ char *argv[]; sampling frequency differences. */ wfdbquiet(); if (orec && (osigopen(orec, dfout, (unsigned)nosig) != nosig)) { - (void)fprintf(stderr, "%s: can't write output signals\n", pname); + /* wfdb-x: say why if an output file is one of the input files (the + library's message, quieted above, is the last error) */ + char *e = wfdberror(); + size_t n = strlen(e), t = strlen(" is also an input file\n"); + + if (!wfdbx_get_legacy() && + strncmp(e, "osigopen: output file ", 22) == 0 && n > 22 + t && + strcmp(e + n - t, " is also an input file\n") == 0) + (void)fprintf(stderr, "%s: can't write output signals (%.*s)\n", + pname, (int)(n - 1), e); + else + (void)fprintf(stderr, "%s: can't write output signals\n", pname); exit(2); } wfdbverbose(); @@ -697,51 +778,75 @@ char *argv[]; appropriate number of bits will be dropped from or added to each signal. 2. If the output gain has been specified, the signal is scaled by - the ratio of the output and input gains. If the input gain is - not specified explicitly, the value WFDB_DEFGAIN is assumed. + the ratio of the output and input gains. (wfdb-x: if the input + signal is uncalibrated, this is an error; the C program, and + legacy mode, assumed an input gain of WFDB_DEFGAIN.) These rules differ from those used by earlier versions of xform, although the default behavior remains the same in most cases. */ if (dfout[i].gain == 0.) { /* output gain not specified */ - if (dfin[j].adcres == 0) { /* input ADC resolution not specified; - assume a suitable default */ - switch (dfin[j].fmt) { - case 80: - case 508: - dfin[j].adcres = 8; - break; - case 212: - dfin[j].adcres = 12; - break; - case 310: - dfin[j].adcres = 10; - break; - case 24: - case 524: - dfin[j].adcres = 24; - break; - case 32: - dfin[j].adcres = 32; - break; - case 16: - case 61: - case 160: - case 516: - default: - /* for historical compatibility, 16-bit formats - assume a resolution of WFDB_DEFRES, although it - would make more sense to assume a resolution of - 16 here */ - dfin[j].adcres = WFDB_DEFRES; - break; + /* wfdb-x: rescaling by ADC resolution needs both resolutions (the + C program guessed an unspecified input resolution from the + format, assuming WFDB_DEFRES for 16-bit formats, as legacy mode + does). */ + if (wfdbx_get_legacy()) { + if (dfin[j].adcres == 0) { /* input ADC resolution not specified; + assume a suitable default */ + switch (dfin[j].fmt) { + case 80: + case 508: + dfin[j].adcres = 8; + break; + case 212: + dfin[j].adcres = 12; + break; + case 310: + dfin[j].adcres = 10; + break; + case 24: + case 524: + dfin[j].adcres = 24; + break; + case 32: + dfin[j].adcres = 32; + break; + case 16: + case 61: + case 160: + case 516: + default: + /* for historical compatibility, 16-bit formats + assume a resolution of WFDB_DEFRES, although it + would make more sense to assume a resolution of + 16 here */ + dfin[j].adcres = WFDB_DEFRES; + break; + } } } + else if (dfin[j].adcres == 0 || dfout[i].adcres == 0) { + (void)fprintf(stderr, + "%s: ADC resolution of signal %d of record %s is not specified\n", + pname, dfin[j].adcres == 0 ? j : i, + dfin[j].adcres == 0 ? irec : (orec ? orec : nrec)); + wfdbquit(); + exit(2); + } gain[i] = (double)(1L << dfout[i].adcres) / (double)(1L << dfin[j].adcres); } - else if (dfin[j].gain == 0.) /* output gain specified, but input + else if (dfin[j].gain == 0.) { /* output gain specified, but input gain not specified */ - gain[i] = dfout[i].gain/WFDB_DEFGAIN; + if (wfdbx_get_legacy()) /* wfdb-x: as in the C program */ + gain[i] = dfout[i].gain/WFDB_DEFGAIN; + else { + (void)fprintf(stderr, + "%s: signal %d of record %s is uncalibrated; it cannot be " + "rescaled to the specified gain\n", pname, j, irec); + wfdbquit(); + exit(2); + } + } else /* input and output gains specified */ gain[i] = dfout[i].gain/dfin[j].gain; diff --git a/convert/a2m.c b/convert/a2m.c index b76fb76..2cdd7a0 100644 --- a/convert/a2m.c +++ b/convert/a2m.c @@ -80,11 +80,16 @@ char *argv[]; char *ifname = NULL, *oann = "atr", *p, *record = NULL, *prog_name(); int i, type = -1, getcann(); long offset = 0L; + double Ffreq = 0.; unsigned int nann = 2; WFDB_Anninfo afarray[2]; static WFDB_Annotation annot; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + /* Read and interpret command-line arguments. */ pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { @@ -98,6 +103,17 @@ char *argv[]; } oann = argv[i]; break; + case 'F': /* sampling frequency follows (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } + break; case 'h': /* help requested */ help(); exit(1); @@ -140,6 +156,7 @@ char *argv[]; } break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -191,8 +208,24 @@ char *argv[]; exit(2); /* Set the time shift. */ - if (sampfreq(record) == 0.) - (void)setsampfreq(250.); + /* wfdb-x: if the record header does not give the sampling frequency, + -F must give it. (The C program, like legacy mode, assumed the rate of + the AHA database, 250 Hz, if a header without a frequency existed, and + 1 sample per second if there was no header.) */ + if (wfdbx_get_legacy()) { + if (sampfreq(record) == 0.) + (void)setsampfreq(250.); + } + else if (sampfreq(record) <= 0.) { + if (Ffreq > 0.) + (void)setsampfreq(Ffreq); + else { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } + } if (offset) offset = strtim(argv[offset]); else { switch (type) { @@ -227,16 +260,27 @@ char *argv[]; break; } break; + /* Times in milliseconds are converted at the sampling frequency of + the record (wfdb-x; the C program assumed 4 ms per sample, i.e., + 250 Hz, whatever the record's frequency, as legacy mode does). */ case 1: case 2: while (getann(0, &annot) >= 0) { - annot.time = (annot.time >> 2) + offset; /* 4 msec/sample */ + if (wfdbx_get_legacy()) + annot.time = (annot.time >> 2) + offset; /* 4 msec/sample */ + else + annot.time = (WFDB_Time)(annot.time * sampfreq(NULL) / 1000.) + + offset; (void)putann(0, &annot); } break; case 3: while (getcann(&annot) >= 0) { - annot.time = (annot.time >> 2) + offset; /* 4 msec/sample */ + if (wfdbx_get_legacy()) + annot.time = (annot.time >> 2) + offset; /* 4 msec/sample */ + else + annot.time = (WFDB_Time)(annot.time * sampfreq(NULL) / 1000.) + + offset; (void)putann(0, &annot); } break; @@ -309,6 +353,30 @@ char *s; } static char *help_strings[] = { + "usage: %s -i AHAFILE [ OPTIONS ... ]\n", + "where AHAFILE is the name of the AHA-format input annotation file,", + "and OPTIONS may include:", + " -a ANNOTATOR specify the output annotator name (default: 'atr')", + " -F FREQ sampling frequency, if the record header does not specify", + " one (AHA database: 250)", + " -h print this usage summary", + " -r RECORD specify the name of the output RECORD (default: use the", + " input file name, less path info and extension, as RECORD)", + " -s TIME shift annotations by TIME", + " (defaults: 0 for TYPE 0,", + " 5:0 for TYPE 1, (or TYPE 3 if RECORD=x2xx or x3xx)", + " 2:30:0 for TYPE 2, (or TYPE 3 if RECORD=x0xx or x1xx)", + " -t TYPE specify type of file to be converted:", + " 0 for files produced by WFDB applications in AHA format,", + " 1 for short-format tape files,", + " 2 for long-format tape files, or", + " 3 for compressed files from AHA DB CD-ROMs or diskettes", + " (defaults: 3 if AHAFILE ends in .ANO or .ano, else 0)", + NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -i AHAFILE [ OPTIONS ... ]\n", "where AHAFILE is the name of the AHA-format input annotation file,", "and OPTIONS may include:", @@ -332,8 +400,9 @@ static char *help_strings[] = { void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) + (void)fprintf(stderr, "%s\n", hs[i]); } diff --git a/convert/ad2m.c b/convert/ad2m.c index c693660..dc9cc54 100644 --- a/convert/ad2m.c +++ b/convert/ad2m.c @@ -51,13 +51,28 @@ int argc; char *argv[]; { char dfname[FNLEN], *ifname = NULL, *p, *record = NULL, *prog_name(); - int cflag = 0, i, v[2], getcvec(); + char *Uunits = NULL, *fstr = NULL, *tstr = NULL; + double Ffreq = 0., Ggain = 0.; + int cflag = 0, i, v[2], getcvec(), Gset = 0, Rres = 0; long t = 0L, start_time = 0L, end_time = 0L; static WFDB_Siginfo dfarray[2]; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); - (void)setsampfreq(250.); + if (wfdbx_get_legacy()) { + /* wfdb-x: in legacy mode, the parameters of the AHA database, set by + the C program (250 Hz, 400 adu/mV, 12 bits, and no units, i.e., + millivolts); -F, -G, -R and -U are not options then. */ + Ffreq = 250.; + Ggain = 400.; + Gset = 1; + Rres = 12; + (void)setsampfreq(250.); + } for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { case 'c': @@ -69,7 +84,10 @@ char *argv[]; "%s: start time must follow -f\n", pname); exit(1); } - start_time = strtim(argv[i]); + if (wfdbx_get_legacy()) /* wfdb-x: as in the C program */ + start_time = strtim(argv[i]); + else + fstr = argv[i]; /* converted once -F is known (wfdb-x) */ break; case 'h': /* help requested */ help(); @@ -98,9 +116,56 @@ char *argv[]; "%s: end time must follow -t\n", pname); exit(1); } - end_time = strtim(argv[i]); + if (wfdbx_get_legacy()) /* wfdb-x: as in the C program */ + end_time = strtim(argv[i]); + else + tstr = argv[i]; /* converted once -F is known (wfdb-x) */ + break; + case 'F': /* sampling frequency (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } break; + case 'G': /* gain in adu per physical unit (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ggain) != 1 || + Ggain < 0.) { + (void)fprintf(stderr, "%s: gain (>= 0) must follow -G\n", + pname); + exit(1); + } + Gset = 1; + break; + case 'R': /* ADC resolution in bits (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%d", &Rres) != 1 || + Rres < 1 || Rres > 32) { + (void)fprintf(stderr, + "%s: ADC resolution (1-32 bits) must follow -R\n", + pname); + exit(1); + } + break; + case 'U': /* physical units (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc) { + (void)fprintf(stderr, "%s: units must follow -U\n", pname); + exit(1); + } + Uunits = argv[i]; + break; + default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -111,7 +176,39 @@ char *argv[]; exit(1); } } - if (ifname == NULL || (0L < end_time && end_time <= start_time)) { + if (ifname == NULL) { + help(); + exit(1); + } + /* wfdb-x: the signal parameters must be given explicitly; the C program + assumed those of the AHA database (250 Hz, 400 adu/mV, 12 bits, and no + units, i.e., millivolts), as legacy mode does (the values set above for + legacy mode pass these checks, and no units are required then). */ + if (Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: the sampling frequency must be specified (-F)\n", + pname); + exit(1); + } + if (!Gset) { + (void)fprintf(stderr, "%s: the gain must be specified (-G)\n", pname); + exit(1); + } + if (Rres < 1) { + (void)fprintf(stderr, + "%s: the ADC resolution must be specified (-R)\n", + pname); + exit(1); + } + if (Ggain != 0. && Uunits == NULL && !wfdbx_get_legacy()) { + (void)fprintf(stderr, "%s: the units must be specified (-U)\n", + pname); + exit(1); + } + (void)setsampfreq(Ffreq); + if (fstr) start_time = strtim(fstr); + if (tstr) end_time = strtim(tstr); + if (0L < end_time && end_time <= start_time) { help(); exit(1); } @@ -160,9 +257,10 @@ char *argv[]; (void)sprintf(dfname, "%s.dat", record); dfarray[0].fname = dfarray[1].fname = dfname; dfarray[0].desc = dfarray[1].desc = ""; - dfarray[0].gain = dfarray[1].gain = 400; + dfarray[0].gain = dfarray[1].gain = Ggain; + dfarray[0].units = dfarray[1].units = Uunits; dfarray[0].fmt = dfarray[1].fmt = 212; - dfarray[0].adcres = dfarray[1].adcres = 12; + dfarray[0].adcres = dfarray[1].adcres = Rres; if (osigfopen(dfarray, 2) < 0) exit(3); if (!cflag) { /* process tape-format input file */ @@ -250,6 +348,28 @@ char *s; } static char *help_strings[] = { + "usage: %s -i AHAFILE [ OPTIONS ... ]\n", + "where AHAFILE is the name of the AHA-format input signal file,", + "and OPTIONS may include:", + " -c process a compressed input file (default if AHAFILE ends", + " with .CMP or .cmp; otherwise old AHA tape format assumed)", + " -f TIME begin at specified TIME (default: 0, i.e., the beginning", + " of the input file)", + " -F FREQ sampling frequency in Hz (required; AHA database: 250)", + " -G GAIN gain in adu per physical unit (required; AHA database: 400)", + " -h print this usage summary", + " -r RECORD specify the name of the output RECORD (default: use the," + " input file name, less path info and extension, as RECORD)", + " -R BITS ADC resolution in bits (required; AHA database: 12)", + " -t TIME stop at specified TIME (default: 35 minutes after the", + " starting time if RECORD=x2xx or x3xx, 3 hours if", + " RECORD=x0xx or x1xx, or the end of the input file)", + " -U UNITS physical units (required unless the gain is 0; AHA: mV)", + NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -i AHAFILE [ OPTIONS ... ]\n", "where AHAFILE is the name of the AHA-format input signal file,", "and OPTIONS may include:", @@ -269,8 +389,9 @@ static char *help_strings[] = { void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) + (void)fprintf(stderr, "%s\n", hs[i]); } diff --git a/convert/ahaecg2mit.c b/convert/ahaecg2mit.c index 3a2cdb7..13fc578 100644 --- a/convert/ahaecg2mit.c +++ b/convert/ahaecg2mit.c @@ -4,10 +4,23 @@ Convert *.ecg or *.txt files from an AHA Database DVD to WFDB-compatible format */ #include +#include #include #include int format; + +/* wfdb-x: the signal parameters (-F, -G, -R, -U); the C program assumed those + of the AHA database (250 Hz, 400 adu/mV, 12 bits, millivolts). */ +static char *pname, *Uunits; +static double Ffreq, Ggain; +static int Gset, Rres; + +/* A long-format AHA record begins with 145 minutes that are not part of the + short-format (35-minute) record; each sample frame of a binary (.ecg) file + is 5 bytes long. */ +#define SKIP_MINUTES 145L +#define FRAME_BYTES 5L FILE *ifile; WFDB_Annotation a; WFDB_Sample v[2]; @@ -59,9 +72,17 @@ main(int argc, char **argv) char *p, *record; int i, sflag = 0; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + if (argc < 2 || strcmp(argv[1], "-h") == 0) { - fprintf(stderr, "usage: %s [-s] RECORD.XXX [RECORD.XXX]...\n", - argv[0]); + if (wfdbx_get_legacy()) /* wfdb-x: the usage of the C program */ + fprintf(stderr, "usage: %s [-s] RECORD.XXX [RECORD.XXX]...\n", + argv[0]); + else + fprintf(stderr, "usage: %s [-s] -F FREQ -G GAIN -R BITS -U UNITS" + " RECORD.XXX [RECORD.XXX]...\n", argv[0]); fprintf(stderr, " where RECORD.XXX is the name of the AHA DVD input file to" " be converted\n" @@ -75,11 +96,94 @@ main(int argc, char **argv) " long-format (2.5 hour)\n input files. When using -s, the" " second digit in the output record name is\n" " incremented by 2.\n"); + if (!wfdbx_get_legacy()) + fprintf(stderr, + " -F, -G, -R and -U give the sampling frequency (Hz), gain" + " (adu per physical\n unit), ADC resolution (bits) and units" + " of the signals; for the AHA DVD\n files, use -F 250 -G 400" + " -R 12 -U mV.\n"); + exit(1); + } + /* wfdb-x: options (-s and the signal parameters) precede the input + files, in any order. */ + pname = argv[0]; + if (wfdbx_get_legacy()) { + /* wfdb-x: in legacy mode, the option of the C program (-s only, as + the first argument), and the parameters of the AHA database that + it set (250 Hz, 400 adu/mV, 12 bits, millivolts). */ + if (strcmp(argv[i = 1], "-s") == 0) { + sflag = 1; /* produce short-format (35-minute) records */ + i = 2; + } + Ffreq = 250.; + Ggain = 400.; + Gset = 1; + Rres = 12; + Uunits = "mV"; + } + else for (i = 1; i < argc && argv[i][0] == '-' && argv[i][1] && + strchr("sFGRU", argv[i][1]) && argv[i][2] == '\0'; i++) + switch (argv[i][1]) { + case 's': + sflag = 1; /* produce short-format (35-minute) records */ + break; + case 'F': /* sampling frequency (wfdb-x) */ + if (++i >= argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } + break; + case 'G': /* gain in adu per physical unit (wfdb-x) */ + if (++i >= argc || sscanf(argv[i], "%lf", &Ggain) != 1 || + Ggain < 0.) { + (void)fprintf(stderr, "%s: gain (>= 0) must follow -G\n", + pname); + exit(1); + } + Gset = 1; + break; + case 'R': /* ADC resolution in bits (wfdb-x) */ + if (++i >= argc || sscanf(argv[i], "%d", &Rres) != 1 || + Rres < 1 || Rres > 32) { + (void)fprintf(stderr, + "%s: ADC resolution (1-32 bits) must follow -R\n", + pname); + exit(1); + } + break; + case 'U': /* physical units (wfdb-x) */ + if (++i >= argc) { + (void)fprintf(stderr, "%s: units must follow -U\n", pname); + exit(1); + } + Uunits = argv[i]; + break; + } + /* wfdb-x: the signal parameters must be given explicitly (see + docs/DIVERGENCES.md); in legacy mode, these checks pass. */ + if (Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: the sampling frequency must be specified (-F)\n", + pname); exit(1); } - if (strcmp(argv[i = 1], "-s") == 0) { - sflag = 1; /* produce short-format (35-minute) records */ - i = 2; + if (!Gset) { + (void)fprintf(stderr, "%s: the gain must be specified (-G)\n", pname); + exit(1); + } + if (Rres < 1) { + (void)fprintf(stderr, + "%s: the ADC resolution must be specified (-R)\n", + pname); + exit(1); + } + if (Ggain != 0. && Uunits == NULL) { + (void)fprintf(stderr, "%s: the units must be specified (-U)\n", + pname); + exit(1); } for ( ; i < argc; i++) { p = argv[i] + strlen(argv[i]) - 4; /* pointer to '.ecg' or '.txt' */ @@ -98,12 +202,14 @@ main(int argc, char **argv) *p = '\0'; /* strip off extension */ record = p - 4; /* AHA record names are 4 (ASCII) digits */ if (sflag) {/* skip first 145 minutes if making a short-format record */ + long nskip = (long)(SKIP_MINUTES*60L*Ffreq); /* samples */ + if (format == 1) - fseek(ifile, 145L*60L*250L*5L, SEEK_SET); + fseek(ifile, nskip*FRAME_BYTES, SEEK_SET); else if (format == 2) { long t; - for (t = 0; t < 145L*60L*250L; t++) + for (t = 0; t < nskip; t++) (void)readtxtdata(); } record[1] += 2; /* fix record name (n0nn->n2nn, n1nn->n3nn) */ @@ -122,14 +228,14 @@ void process(char *record) static WFDB_Siginfo si[2]; static WFDB_Anninfo ai; - setsampfreq(250.0); /* AHA DB is sampled at 250 Hz for each signal */ + setsampfreq(Ffreq); /* wfdb-x: -F (the C program set 250 Hz) */ sprintf(ofname, "%s.dat", record); si[0].fname = ofname; si[0].desc = "ECG0"; - si[0].units = "mV"; - si[0].gain = 400; + si[0].units = Uunits; + si[0].gain = Ggain; si[0].fmt = 212; - si[0].adcres = 12; + si[0].adcres = Rres; si[1] = si[0]; si[1].desc = "ECG1"; ai.name = "atr"; diff --git a/convert/edf2mit.c b/convert/edf2mit.c index 9bf0ee7..fdd96b8 100644 --- a/convert/edf2mit.c +++ b/convert/edf2mit.c @@ -44,6 +44,10 @@ char **argv; WFDB_Siginfo *si, *so; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = argv[0]; record[0] = '\0'; for (i = 1; i < argc; i++) { diff --git a/convert/m2a.c b/convert/m2a.c index 814ce16..821e7ea 100644 --- a/convert/m2a.c +++ b/convert/m2a.c @@ -48,11 +48,16 @@ char *argv[]; { char *ianame = NULL, *oaname = NULL, *record = NULL, *shift = NULL; char *prog_name(); + double Ffreq = 0.; int i; WFDB_Anninfo afarray[2]; WFDB_Annotation annot; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { @@ -66,6 +71,17 @@ char *argv[]; ianame = argv[i++]; oaname = argv[i]; break; + case 'F': /* sampling frequency follows (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ffreq) != 1 || + Ffreq <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency (> 0) must follow -F\n", + pname); + exit(1); + } + break; case 'h': /* help requested */ help(); exit(1); @@ -88,6 +104,7 @@ char *argv[]; break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -114,7 +131,21 @@ char *argv[]; long dt; double mssi; /* milliseconds per sample interval */ - if (sampfreq(record) <= 0.) (void)setsampfreq(WFDB_DEFFREQ); + /* wfdb-x: no default sampling frequency (the C program assumed 250 Hz + if the record header did not specify one, as legacy mode does); -F + specifies one. */ + if (sampfreq(record) <= 0.) { + if (wfdbx_get_legacy()) + (void)setsampfreq(WFDB_DEFFREQ); + else if (Ffreq > 0.) + (void)setsampfreq(Ffreq); + else { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified (use -F)\n", + pname, record); + exit(1); + } + } mssi = 1000./strtim("1"); dt = strtim(shift); while (getann(0, &annot) >= 0) @@ -149,6 +180,19 @@ char *s; } static char *help_strings[] = { + "usage: %s -r RECORD -a MIT-ANNOTATOR AHA-ANNOTATOR [ OPTIONS ... ]\n", + "where RECORD is the record name, MIT-ANNOTATOR is the annotator name", + "for the input annotation file, AHA-ANNOTATOR is the annotator name", + "for the output (AHA format) annotation file, and OPTIONS may include:", + " -F FREQ sampling frequency, if the record header does not specify one", + " -h print this usage summary", + " -s TIME shift annotation times backwards by the specified TIME,", + " and convert them to milliseconds", + NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -r RECORD -a MIT-ANNOTATOR AHA-ANNOTATOR [ OPTIONS ... ]\n", "where RECORD is the record name, MIT-ANNOTATOR is the annotator name", "for the input annotation file, AHA-ANNOTATOR is the annotator name", @@ -162,8 +206,9 @@ static char *help_strings[] = { void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) + (void)fprintf(stderr, "%s\n", hs[i]); } diff --git a/convert/makeid.c b/convert/makeid.c index 674f438..5408036 100644 --- a/convert/makeid.c +++ b/convert/makeid.c @@ -57,6 +57,10 @@ char *argv[]; WFDB_Siginfo dfarray[2]; WFDB_Annotation annot; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + if (argc < 3) { (void)fprintf(stderr, "usage: %s annotator record\n", argv[0]); exit(1); diff --git a/convert/md2a.c b/convert/md2a.c index 6d275c9..d38beb7 100644 --- a/convert/md2a.c +++ b/convert/md2a.c @@ -57,6 +57,10 @@ char *argv[]; static WFDB_Siginfo s[WFDB_MAXSIG]; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { diff --git a/convert/mit2edf.c b/convert/mit2edf.c index 715848d..64177af 100644 --- a/convert/mit2edf.c +++ b/convert/mit2edf.c @@ -48,6 +48,10 @@ char **argv; "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + /* Interpret the command line. */ pname = argv[0]; for (i = 1; i < argc; i++) { @@ -115,6 +119,30 @@ char **argv; if ((nsig = isigopen(record, si, nsig)) <= 0) exit(3); + /* wfdb-x: an EDF file needs the sampling frequency and the gain of each + signal (the C program assumed 250 Hz and 200 adu/mV if the header did + not specify them, as legacy mode does). */ + if (!wfdbx_get_legacy() && sampfreq(NULL) <= 0.) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + pname, record); + exit(3); + } + for (i = 0; i < nsig && !wfdbx_get_legacy(); i++) { + if (si[i].gain == 0.) { + (void)fprintf(stderr, + "%s: signal %d of record %s is uncalibrated\n", + pname, i, record); + exit(3); + } + if (si[i].adcres < 1) { /* the EDF digital range needs it */ + (void)fprintf(stderr, + "%s: ADC resolution of signal %d of record %s is not specified\n", + pname, i, record); + exit(3); + } + } + /* Open the output (EDF) file. */ if ((ofile = fopen(ofname, "wb")) == NULL) { (void)fprintf(stderr, "%s: can't create %s\n", pname, ofname); @@ -201,7 +229,10 @@ char **argv; /* Calculate physical and digital extrema. */ for (i = 0; i < nsig; i++) { - if (si[i].adcres < 1) { /* invalid ADC resolution in input .hea file */ + /* (wfdb-x: the ADC resolution was checked above; the C program guessed + it from the storage format if the header did not give it, as + legacy mode does.) */ + if (wfdbx_get_legacy() && si[i].adcres < 1) { /* invalid ADC resolution in input .hea file */ switch (si[i].fmt) { /* guess ADC resolution based on format */ case 524: case 24: si[i].adcres = 24; break; @@ -313,7 +344,8 @@ char **argv; /* Physical dimension (e.g., uV or degreeC). */ for (i = 0; i < nsig; i++, p += 8) { - if (si[i].units == NULL) si[i].units = "mV"; + if (si[i].units == NULL) /* wfdb-x: unspecified units are shown as "nd" (C, legacy mode: mV) */ + si[i].units = wfdbx_get_legacy() ? "mV" : "nd"; else if (strlen(si[i].units) > 8) si[i].units[8] = '\0'; strncpy(p, si[i].units, strlen(si[i].units)); } @@ -413,7 +445,7 @@ char **argv; if (n % blocks_per_hour == 0) printf("\n"); } } - (void)fclose(ofile); + (void)wfdbx_fclose_output(ofile, pname, ofname); /* wfdb-x */ printf("\n"); if (edfplusflag) { diff --git a/convert/mit2wav.c b/convert/mit2wav.c index 4fc7431..901d6a7 100644 --- a/convert/mit2wav.c +++ b/convert/mit2wav.c @@ -140,6 +140,10 @@ main(int argc, char **argv) static WFDB_Sample *x, *y; static WFDB_Siginfo *s; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + /* Interpret the command line. */ pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { @@ -215,6 +219,13 @@ main(int argc, char **argv) } if ((nsig = isigopen(record, s, nsig)) <= 0) exit(3); + if (!wfdbx_get_legacy() && sampfreq(NULL) <= 0.) { + /* wfdb-x: no default frequency (except in legacy mode) */ + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + pname, record); + exit(3); + } /* Check that the new record name is legal before proceeding. Note that this step creates a header file, which is overwritten after the diff --git a/convert/parsescp.c b/convert/parsescp.c index 1b46863..ca68aa0 100644 --- a/convert/parsescp.c +++ b/convert/parsescp.c @@ -602,7 +602,7 @@ int write_output() fprintf(dfile, "T axis: %d\n", Taxis); precgmeas(dfile); - fclose(dfile); + wfdbx_fclose_output(dfile, pname, dname); /* wfdb-x */ /* Generate the key file. The patient's name and ID number should appear only in this file (and in the debugging output, if @@ -611,7 +611,7 @@ int write_output() fprintf(kfile, "Record: %s (%s %s)\n", recname, recdate, rectime); fprintf(kfile, "Name: %s\n", patient_name); fprintf(kfile, "MRN: %s\n", patient_id); - fclose(kfile); + wfdbx_fclose_output(kfile, pname, kname); /* wfdb-x */ /* If the -z option was selected, then for each signal: 1. Calculate the median amplitude of the first 5000 samples in the @@ -679,7 +679,7 @@ int write_output() fprintf(tfile, "%7d", ecg[ V6][i]); fprintf(tfile, "\n"); } - fclose(tfile); + wfdbx_fclose_output(tfile, pname, tname); /* wfdb-x */ } /* Finally, output the ECG in vector (binary) form. @@ -709,7 +709,7 @@ int write_output() fwriteswapped(ecg[ V3]+2500, 2, 1250, efile); fwriteswapped(ecg[ V6]+3750, 2, 1250, efile); fwriteswapped(ecg[ leadII] , 2, 5000, efile); - fclose(efile); + wfdbx_fclose_output(efile, pname, ename); /* wfdb-x */ return (1); } @@ -736,16 +736,30 @@ int write_wfdb_record() } sprintf(filename, "%s.dat", recname); + /* wfdb-x: the sampling frequency is the one given in the SCP file (the C + program always wrote 500 Hz, as legacy mode does); without one, no + record is written. */ + if (!wfdbx_get_legacy()) { + if (sfreq <= 0.) { + fprintf(stderr, "%s: sampling frequency not specified; record %s not " + "written\n", pname, recname); + return (0); + } + setsampfreq(sfreq); + } if (newheader(recname) < 0) return(0); - setsampfreq(500.); /* FIXME: we should calculate this! */ + if (wfdbx_get_legacy()) + setsampfreq(500.); /* FIXME: we should calculate this! */ for (i = 0; i < 12; i++) { s[i].fname = filename; s[i].desc = leadname[i]; s[i].units = "mV"; s[i].fmt = 16; - s[i].gain = 200; /* FIXME: we should calculate this! */ + s[i].gain = 200; /* wfdb-x: exact, not assumed: the samples have + been rescaled to units of 5000 nV (see + gref and gres), i.e., 200 adu/mV */ s[i].adcres = 16; /* FIXME: we should calculate this! */ } if (osigfopen(s, 12) < 12) @@ -822,6 +836,10 @@ main(int argc, char **argv) FILE *ofile, *vfile; char *prog_name(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { @@ -1954,10 +1972,12 @@ int Huffdecode(unsigned char *p, long len, int section) code in main() */ sfreq = 1.0e6/sampint; } - else { + else if (wfdbx_get_legacy()) { /* wfdb-x: as in the C program */ printf("Error: sampling frequency not specified; assuming 500 Hz\n"); sfreq = 500; } + else /* wfdb-x: the C program assumed 500 Hz */ + printf("Error: sampling frequency not specified\n"); if (vflag) printf(" Encoding type: %d ", *(p+3)); switch (*(p+3)) { case 0: if (vflag) printf("(amplitudes)\n"); enctype = 0; break; diff --git a/convert/rdedfann.c b/convert/rdedfann.c index 8af375f..27fad9c 100644 --- a/convert/rdedfann.c +++ b/convert/rdedfann.c @@ -48,6 +48,10 @@ main(int argc, char **argv) WFDB_Siginfo *si; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { diff --git a/convert/readid.c b/convert/readid.c index 425595a..d6c1840 100644 --- a/convert/readid.c +++ b/convert/readid.c @@ -30,6 +30,8 @@ _______________________________________________________________________________ #else extern void exit(); #endif +#include /* wfdb-x: for wfdbx_legacy_args */ + g16(fp) FILE *fp; @@ -49,6 +51,10 @@ char *argv[]; long dcount, acount; static char buf[512], name[9]; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + if (argc < 3) { (void)fprintf(stderr, "usage: %s data-block-size annot-block-size\n", argv[0]); diff --git a/convert/revise.c b/convert/revise.c index 12619f9..016977b 100644 --- a/convert/revise.c +++ b/convert/revise.c @@ -48,6 +48,10 @@ char *argv[]; char *record; int nsig; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + if (argc < 2) { (void)fprintf(stderr, "usage: %s record\n", argv[0]); (void)fprintf(stderr, @@ -191,7 +195,8 @@ WFDB_Siginfo *siarray; /* Determine the ADC resolution. */ siarray[s].adcres = (p = strtok((char *)NULL, sep)) ? - atoi(p) : WFDB_DEFRES; + atoi(p) : (wfdbx_get_legacy() ? WFDB_DEFRES : 0); + /* wfdb-x: C (and legacy mode) assumed WFDB_DEFRES */ /* Determine the ADC zero. */ siarray[s].adczero = (p = strtok((char *)NULL, sep)) ? atoi(p) : 0; diff --git a/convert/wav2mit.c b/convert/wav2mit.c index 6fdf1b5..49e1223 100644 --- a/convert/wav2mit.c +++ b/convert/wav2mit.c @@ -86,15 +86,32 @@ void help(void); main(int argc, char **argv) { char buf[80], *ifname, *record = NULL; - double sps, wgain; + char *Uunits = NULL; + double sps, Ggain = 0.; + int Gset = 0; int bitspersample, bytespersecond, framelen, i, nsig, tag, wres; long flen, len, wnsamp; static WFDB_Siginfo *s; + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + /* Interpret the command line. */ pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { + case 'G': /* gain in adu per physical unit (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc || sscanf(argv[i], "%lf", &Ggain) != 1 || + Ggain < 0.) { + (void)fprintf(stderr, "%s: gain (>= 0) must follow -G\n", + pname); + exit(1); + } + Gset = 1; + break; case 'h': /* help requested */ help(); exit(1); @@ -122,7 +139,17 @@ main(int argc, char **argv) } record = argv[i]; break; + case 'U': /* physical units (wfdb-x) */ + if (wfdbx_get_legacy()) /* not an option of the original */ + goto unrecognized; + if (++i >= argc) { + (void)fprintf(stderr, "%s: units must follow -U\n", pname); + exit(1); + } + Uunits = argv[i]; + break; default: + unrecognized: /* wfdb-x: also the wfdb-x options, in legacy mode */ (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); @@ -139,6 +166,19 @@ main(int argc, char **argv) help(); exit(1); } + /* wfdb-x: the gain and units must be given explicitly; the C program + wrote 12.5 adu/mV for 8-bit and 6400 adu/mV for 16-bit files, with no + units (millivolts). (The sampling frequency and the resolution are + those given in the file.) Legacy mode: as in the C program. */ + if (!Gset && !wfdbx_get_legacy()) { + (void)fprintf(stderr, "%s: the gain must be specified (-G)\n", pname); + exit(1); + } + if (Ggain != 0. && Uunits == NULL && !wfdbx_get_legacy()) { + (void)fprintf(stderr, "%s: the units must be specified (-U)\n", + pname); + exit(1); + } /* If the record name was not specified, generate it from ifname. */ if (record == NULL) { @@ -198,11 +238,11 @@ main(int argc, char **argv) bitspersample = in16(); if (bitspersample <= 8) { wres = 8; - wgain = 12.5; + if (wfdbx_get_legacy()) Ggain = 12.5; /* wfdb-x: the C program's gain */ } else if (bitspersample <= 16) { wres = 16; - wgain = 6400; + if (wfdbx_get_legacy()) Ggain = 6400; /* wfdb-x: the C program's gain */ } else { fprintf(stderr, "%s: unsupported resolution (%d bits/sample) in %s\n", @@ -233,8 +273,8 @@ main(int argc, char **argv) for (i = 0; i < nsig; i++) { s[i].fname = ifname; s[i].desc = NULL; - s[i].units = NULL; - s[i].gain = wgain; + s[i].units = Uunits; + s[i].gain = Ggain; s[i].initval = 0; s[i].group = 0; s[i].fmt = (wres == 16) ? 16 : 80; @@ -304,6 +344,18 @@ char *prog_name(char *s) } static char *help_strings[] = { + "usage: %s -i FILE.wav -r RECORD [ OPTIONS ... ]\n", + "where FILE.wav is the name of the wav-format input signal file,", + "and OPTIONS may include:", + " -G GAIN gain in adu per physical unit (required; 0: uncalibrated)", + " -h print this usage summary", + " -r RECORD create RECORD.hea (default: FILE.hea)", + " -U UNITS physical units (required unless the gain is 0)", + NULL +}; + +/* wfdb-x: the help text of the original program, printed in legacy mode */ +static char *legacy_help_strings[] = { "usage: %s -i FILE.wav -r RECORD [ OPTIONS ... ]\n", "where FILE.wav is the name of the wav-format input signal file,", "and OPTIONS may include:", @@ -315,8 +367,9 @@ static char *help_strings[] = { void help() { int i; + char **hs = wfdbx_get_legacy() ? legacy_help_strings : help_strings; /* wfdb-x */ - (void)fprintf(stderr, help_strings[0], pname); - for (i = 1; help_strings[i] != NULL; i++) - (void)fprintf(stderr, "%s\n", help_strings[i]); + (void)fprintf(stderr, hs[0], pname); + for (i = 1; hs[i] != NULL; i++) + (void)fprintf(stderr, "%s\n", hs[i]); } diff --git a/convert/wfdb2mat.c b/convert/wfdb2mat.c index 263e0f4..3cdd374 100644 --- a/convert/wfdb2mat.c +++ b/convert/wfdb2mat.c @@ -126,6 +126,10 @@ char *argv[]; WFDB_Time from = 0L, maxl = 0L, t, to = 0L, lower, upper; void help(); + /* wfdb-x: --legacy as the first argument selects legacy mode (the + behavior of the original program; see lib/signal.c) */ + (void)wfdbx_legacy_args(&argc, &argv); + pname = prog_name(argv[0]); for (i = 1 ; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { @@ -208,11 +212,22 @@ char *argv[]; SUALLOC(vi, nisig, sizeof(WFDB_Sample)); if ((nisig = isigopen(record, si, nisig)) <= 0) exit(2); - for (i = 0; i < nisig; i++) - if (si[i].gain == 0.0) si[i].gain = WFDB_DEFGAIN; + /* wfdb-x: uncalibrated signals remain uncalibrated in the output (the C + program assumed 200 adu/mV), and the output header needs the sampling + frequency (the C program assumed 250 Hz). Legacy mode: as in the C + program. */ + if (wfdbx_get_legacy()) + for (i = 0; i < nisig; i++) + if (si[i].gain == 0.0) si[i].gain = WFDB_DEFGAIN; if (highres) setgvmode(WFDB_HIGHRES); freq = sampfreq(NULL); + if (freq <= 0. && !wfdbx_get_legacy()) { + (void)fprintf(stderr, + "%s: sampling frequency of record %s is not specified\n", + pname, record); + exit(2); + } if (from > 0L && (from = strtim(argv[from])) < 0L) from = -from; if (isigsettime(from) < 0) @@ -345,8 +360,10 @@ char *argv[]; so[i].baseline -= si[sig[i]].adczero; /* handle possibly missing units strings */ if (so[i].units == NULL) { - /* in a .hea file, missing units can be assumed to be millivolts */ - SSTRCPY(so[i].units, "mV"); + /* wfdb-x: missing units are not assumed to be millivolts (except in + legacy mode: "in a .hea file, missing units can be assumed to + be millivolts") */ + SSTRCPY(so[i].units, wfdbx_get_legacy() ? "mV" : "nd"); } else if (strlen(so[i].units) == 0) { /* this can happen only in an EDF file; in this case, the signal diff --git a/lib/annot.c b/lib/annot.c index d90ca9a..e22a915 100644 --- a/lib/annot.c +++ b/lib/annot.c @@ -93,6 +93,16 @@ earlier versions. Beginning in version 10.4.12, the canonical annotation order is given by the time, num, and chan fields in that order. Thus simultaneous annotations may be attached to the same signal provided that their num fields are unique. + +wfdb-x: oannclose rearranges an out-of-order output annotation file itself, +in the same process (sort_ann_file), with a result identical to that of the +sortann program, which the C library ran through the shell (and found only if +it was in the PATH); decided by Reza Sameni after a code review, Oct 2026. +wfdb-x: the sort no longer loses an out-of-order annotation at time 0 (num +0, chan 0), nor those inserted after it, as sortann did (decided by Reza +Sameni, Oct 2026). +The original behavior remains available with --legacy (see +docs/DIVERGENCES.md). */ #include "wfdblib.h" @@ -176,6 +186,7 @@ static struct oadata { WFDB_Frequency afreq; /* time resolution, in ticks/second */ int seqno; /* annotation serial number (AHA format only)*/ char *rname; /* record with which annotator is associated */ + char *fname; /* wfdb-x: name of the file (as created) */ char out_of_order; /* if >0, one or more annotations written by putann are not in the canonical (time, num, chan) order */ @@ -186,6 +197,18 @@ static WFDB_Frequency oafreq; /* time resolution in ticks/sec for newly- static int annclose_error; /* if <0, error occurred while closing annotation files */ +/* wfdb-x: a table of annotation type definitions (mnemonics, descriptions, + and flags marking the modified types). lib_defs is that of the library + (astring, tstring, and modified, defined below); sort_ann_file uses one of + its own. */ +struct ann_defs { + char **astr; /* mnemonic strings */ + char **tstr; /* descriptive strings */ + char *mod; /* mod[i] is non-zero if type i was modified */ +}; +static char *astring[ACMAX+1], *tstring[ACMAX+1], modified[ACMAX+1]; +static struct ann_defs lib_defs = { astring, tstring, modified }; + #ifdef WFDB_LARGETIME typedef unsigned long long unsigned_time; #else @@ -221,25 +244,32 @@ static WFDB_Time round_to_time(double x) } } -static int get_ann_table(WFDB_Annotator i) +static int getann_ia(struct iadata *ia, WFDB_Annotation *annot); +static int ungetann_ia(struct iadata *ia, const WFDB_Annotation *annot); +static int set_def_string(char **table, char *mod, int code, + const char *string); + +/* wfdb-x: read_ann_table is the body of get_ann_table, for the input + annotator ia and the definition table defs (the library's, or that of + sort_ann_file). */ +static int read_ann_table(struct iadata *ia, struct ann_defs *defs) { char *p1, *p2; int a; WFDB_Annotation annot; - WFDB_Frequency sfreq; - iad[i]->tmul = 1.0; - iad[i]->afreq = 0.0; + ia->tmul = 1.0; + ia->afreq = 0.0; - if (getann(i, &annot) < 0) /* prime the pump */ + if (getann_ia(ia, &annot) < 0) /* prime the pump */ return (-1); - while (getann(i,&annot) == 0 && annot.time == 0L && + while (getann_ia(ia, &annot) == 0 && annot.time == 0L && annot.anntyp == NOTE && annot.subtyp == 0) { if (annot.aux == NULL || *annot.aux < 1) continue; if (*(annot.aux+1) == '#') { if (strncmp((char *)annot.aux+1, "## time resolution: ", 20) == 0) - sscanf((char *)annot.aux + 20, "%lf", &(iad[i]->afreq)); + sscanf((char *)annot.aux + 20, "%lf", &(ia->afreq)); continue; } p1 = (char *) annot.aux + 1; @@ -252,18 +282,24 @@ static int get_ann_table(WFDB_Annotator i) p2 = p1 + strcspn(p1, " \t"); /* non-whitespace (mnemonic) */ if (p2 != p1) { if (*p2) - setanndesc(a, p2 + 1); + set_def_string(defs->tstr, defs->mod, a, p2 + 1); else - setanndesc(a, (char *)NULL); + set_def_string(defs->tstr, defs->mod, a, (char *)NULL); *p2 = 0; - setannstr(a, p1); + set_def_string(defs->astr, defs->mod, a, p1); } } if (annot.time != 0L || annot.anntyp != NOTE || annot.subtyp != 0 || annot.aux == NULL) { - (void)ungetann(i, &annot); + (void)ungetann_ia(ia, &annot); } + return (0); +} +static int get_ann_table(WFDB_Annotator i) +{ + if (read_ann_table(iad[i], &lib_defs) < 0) + return (-1); setiafreq(i, getifreq()); return (0); } @@ -272,7 +308,14 @@ static char modified[ACMAX+1]; /* modified[i] is non-zero if setannstr() or setanndesc() has modified the mnemonic or description for annotation type i */ -static int put_ann_table(WFDB_Annotator i) +static int put_ann_word(struct oadata *oa, WFDB_Annotator n, + const WFDB_Annotation *annot); + +/* wfdb-x: put_ann_table writes the table to the output annotator oa (number + an), with the time resolution freq and the definition table defs (the C + library used oad[an], oafreq, and the library's table). */ +static int put_ann_table(struct oadata *oa, WFDB_Annotator an, + WFDB_Frequency freq, const struct ann_defs *defs) { int a, flag = 0, n; char buf[256], *str = NULL; @@ -282,41 +325,42 @@ static int put_ann_table(WFDB_Annotator i) annot.anntyp = NOTE; annot.subtyp = annot.chan = annot.num = 0; annot.aux = (unsigned char *)buf; - if (oafreq != oad[i]->afreq && oafreq > 0.) { - (void)sprintf(buf+1, "## time resolution: %.12g", oafreq); + if (freq != oa->afreq && freq > 0.) { + (void)sprintf(buf+1, "## time resolution: %.12g", freq); buf[0] = strlen(buf+1); - oad[i]->afreq = oafreq; - if (putann(i, &annot) < 0) return (-1); + oa->afreq = freq; + if (put_ann_word(oa, an, &annot) < 0) return (-1); flag = 1; } for (a = 0; a <= ACMAX; a++) - if (modified[a]) { + if (defs->mod[a]) { if (flag < 2) { /* mark the beginning of the table */ (void)sprintf(buf+1, "## annotation type definitions"); buf[0] = strlen(buf+1); - if (putann(i, &annot) < 0) return (-1); + if (put_ann_word(oa, an, &annot) < 0) return (-1); } - if (anndesc(a)) - n = wfdb_asprintf(&str, "%d %s %s", a, annstr(a), anndesc(a)); + if (defs->tstr[a]) + n = wfdb_asprintf(&str, "%d %s %s", a, defs->astr[a], + defs->tstr[a]); else - n = wfdb_asprintf(&str, "%d %s ", a, annstr(a)); + n = wfdb_asprintf(&str, "%d %s ", a, defs->astr[a]); if (!str) return (-1); annot.aux[0] = (n > 255 ? 255 : n); memcpy(annot.aux + 1, str, n); SFREE(str); - if (putann(i, &annot) < 0) return (-1); + if (put_ann_word(oa, an, &annot) < 0) return (-1); flag = 2; } if (flag == 2) { /* if a table was written, mark its end */ (void)sprintf(buf+1, "## end of definitions"); buf[0] = strlen(buf+1); - if (putann(i, &annot) < 0) return (-1); + if (put_ann_word(oa, an, &annot) < 0) return (-1); } if (flag) { /* if a table was written, mark its end */ annot.anntyp = 0; annot.aux = NULL; - if (putann(i, &annot) < 0) return (-1); + if (put_ann_word(oa, an, &annot) < 0) return (-1); } return (0); @@ -354,13 +398,38 @@ static int allocoann(unsigned n) return (maxoann); } +/* ann_file_format reads the first word of the input annotation file of ia + and returns its format: WFDB_READ (MIT format; any initial null + annotations are then read as well) or WFDB_AHA_READ. (wfdb-x: this was + part of annopen.) */ +static int ann_file_format(struct iadata *ia) +{ + int a; + + /* Try to figure out what format the file is in. AHA-format files + begin with a null byte and an ASCII character which is one + of the legal AHA annotation codes other than '[' or ']'. + MIT annotation files cannot begin in this way. */ + ia->word = (unsigned)wfdb_g16(ia->file); + a = (ia->word >> 8) & 0xff; + if ((ia->word & 0xff) || + ammap(a) == NOTQRS || a == '[' || a == ']') { + /* read any initial null annotation(s) */ + while ((ia->word & CODE) == SKIP) { + ia->tt += wfdb_g32(ia->file); + ia->word = (unsigned)wfdb_g16(ia->file); + } + return (WFDB_READ); + } + return (WFDB_AHA_READ); +} + /* WFDB library functions (for general use). */ /* annopen: open annotation files for the specified record */ FINT annopen(char *record, const WFDB_Anninfo *aiarray, unsigned int nann) { - int a; unsigned int i, niafneeded, noafneeded; annclose_error = 0; @@ -419,14 +488,7 @@ FINT annopen(char *record, const WFDB_Anninfo *aiarray, ia->info.name = NULL; SSTRCPY(ia->info.name, aiarray[i].name); - /* Try to figure out what format the file is in. AHA-format files - begin with a null byte and an ASCII character which is one - of the legal AHA annotation codes other than '[' or ']'. - MIT annotation files cannot begin in this way. */ - ia->word = (unsigned)wfdb_g16(iad[niaf]->file); - a = (ia->word >> 8) & 0xff; - if ((ia->word & 0xff) || - ammap(a) == NOTQRS || a == '[' || a == ']') { + if (ann_file_format(ia) == WFDB_READ) { if (aiarray[i].stat != WFDB_READ) { wfdb_error("warning (annopen, annotator %s, record %s):\n", aiarray[i].name, record); @@ -434,11 +496,6 @@ FINT annopen(char *record, const WFDB_Anninfo *aiarray, wfdb_error(" ... continuing under that assumption\n"); } (ia->info).stat = WFDB_READ; - /* read any initial null annotation(s) */ - while ((ia->word & CODE) == SKIP) { - ia->tt += wfdb_g32(ia->file); - ia->word = (unsigned)wfdb_g16(ia->file); - } } else { if (aiarray[i].stat != WFDB_AHA_READ) { @@ -470,6 +527,8 @@ FINT annopen(char *record, const WFDB_Anninfo *aiarray, SSTRCPY(oa->info.name, aiarray[i].name); oa->rname = NULL; SSTRCPY(oa->rname, record); + oa->fname = NULL; /* wfdb-x: for sort_ann_file */ + SSTRCPY(oa->fname, wfdbfile(NULL, NULL)); oa->ann.time = 0L; oa->info.stat = aiarray[i].stat; oa->out_of_order = 0; @@ -484,13 +543,19 @@ FINT annopen(char *record, const WFDB_Anninfo *aiarray, /* getann: read an annotation from annotator n into *annot */ FINT getann(WFDB_Annotator n, WFDB_Annotation *annot) { - int a, len; struct iadata *ia; if (n >= niaf || (ia = iad[n]) == NULL || ia->file == NULL) { wfdb_error("getann: can't read annotator %d\n", n); return (-2); } + return (getann_ia(ia, annot)); +} + +/* wfdb-x: getann_ia is the body of getann, for the input annotator ia. */ +static int getann_ia(struct iadata *ia, WFDB_Annotation *annot) +{ + int a, len; if (ia->pann.anntyp) { /* an annotation was pushed back */ *annot = ia->pann; @@ -601,34 +666,50 @@ FINT ungetann(WFDB_Annotator n, const WFDB_Annotation *annot) annot->time, n); return (-1); } - iad[n]->pann = *annot; - if (annot->time == iad[n]->prev_time) - iad[n]->pann_tt = iad[n]->prev_tt; + return (ungetann_ia(iad[n], annot)); +} + +/* wfdb-x: ungetann_ia is the body of ungetann (after the checks), for the + input annotator ia, whose pushback buffer is empty. */ +static int ungetann_ia(struct iadata *ia, const WFDB_Annotation *annot) +{ + ia->pann = *annot; + if (annot->time == ia->prev_time) + ia->pann_tt = ia->prev_tt; else - iad[n]->pann_tt = annot->time / iad[n]->tmul; + ia->pann_tt = annot->time / ia->tmul; return (0); } /* putann: write annotation at annot to annotator n */ FINT putann(WFDB_Annotator n, const WFDB_Annotation *annot) { - unsigned annwd; - const unsigned char *ap; - int i, len; - unsigned_time delta; - WFDB_Time t; struct oadata *oa; if (n >= noaf || (oa = oad[n]) == NULL || oa->file == NULL) { wfdb_error("putann: can't write annotation file %d\n", n); return (-2); } - t = annot->time; if (!oa->table_written) { oa->table_written = 1; - if (put_ann_table(n) < 0) + if (put_ann_table(oa, n, oafreq, &lib_defs) < 0) return (-1); } + return (put_ann_word(oa, n, annot)); +} + +/* wfdb-x: put_ann_word is the body of putann (after the table), for the + output annotator oa (number n, in messages). */ +static int put_ann_word(struct oadata *oa, WFDB_Annotator n, + const WFDB_Annotation *annot) +{ + unsigned annwd; + const unsigned char *ap; + int i, len; + unsigned_time delta; + WFDB_Time t; + + t = annot->time; delta = (unsigned_time) t - oa->ann.time; if (!(annot->chan > oa->ann.chan || annot->num > oa->ann.num || t > oa->ann.time || (t == 0L && oa->ann.time == 0L))) @@ -849,18 +930,23 @@ FINT setecgstr(int code, const char *string) return (-1); } -static char *astring[ACMAX+1] = { /* mnemonic strings for each code */ - " ", "N", "L", "R", "a", /* 0 - 4 */ - "V", "F", "J", "A", "S", /* 5 - 9 */ - "E", "j", "/", "Q", "~", /* 10 - 14 */ - "[15]", "|", "[17]", "s", "T", /* 15 - 19 */ - "*", "D", "\"", "=", "p", /* 20 - 24 */ - "B", "^", "t", "+", "u", /* 25 - 29 */ - "?", "!", "[", "]", "e", /* 30 - 34 */ - "n", "@", "x", "f", "(", /* 35 - 39 */ - ")", "r", "[42]", "[43]", "[44]", /* 40 - 44 */ +/* wfdb-x: the default mnemonic strings (for astring and astring0) */ +#define DEFAULT_ASTRINGS \ + " ", "N", "L", "R", "a", /* 0 - 4 */ \ + "V", "F", "J", "A", "S", /* 5 - 9 */ \ + "E", "j", "/", "Q", "~", /* 10 - 14 */ \ + "[15]", "|", "[17]", "s", "T", /* 15 - 19 */ \ + "*", "D", "\"", "=", "p", /* 20 - 24 */ \ + "B", "^", "t", "+", "u", /* 25 - 29 */ \ + "?", "!", "[", "]", "e", /* 30 - 34 */ \ + "n", "@", "x", "f", "(", /* 35 - 39 */ \ + ")", "r", "[42]", "[43]", "[44]", /* 40 - 44 */ \ "[45]", "[46]", "[47]", "[48]", "[49]" /* 45 - 49 */ + +static char *astring[ACMAX+1] = { /* mnemonic strings for each code */ + DEFAULT_ASTRINGS }; +static char *const astring0[ACMAX+1] = { DEFAULT_ASTRINGS }; /* wfdb-x */ FSTRING annstr(int code) { @@ -885,7 +971,11 @@ FINT strann(const char *str) return (NOTQRS); } -FINT setannstr(int code, const char *string) +/* wfdb-x: set_def_string is the body of setannstr and setanndesc, for a + string table (of mnemonics or descriptions) and the modification flags + (mod) of a definition table. It returns 0, or -1 if code is illegal. */ +static int set_def_string(char **table, char *mod, int code, + const char *string) { int mflag = 0; @@ -893,71 +983,82 @@ FINT setannstr(int code, const char *string) else code = -code; if (code <= ACMAX) { if (string == NULL) string = ""; - if (astring[code] == NULL || strcmp(astring[code], string)) { - astring[code] = NULL; - SSTRCPY(astring[code], string); - if (mflag) modified[code] = 1; + if (table[code] == NULL || strcmp(table[code], string)) { + table[code] = NULL; + SSTRCPY(table[code], string); + if (mflag) mod[code] = 1; } return (0); } - else { - wfdb_error("setannstr: illegal annotation code %d\n", code); - return (-1); - } + return (-1); } -static char *tstring[ACMAX+1] = { /* descriptive strings for each code */ - "", - "Normal beat", - "Left bundle branch block beat", - "Right bundle branch block beat", - "Aberrated atrial premature beat", - "Premature ventricular contraction", - "Fusion of ventricular and normal beat", - "Nodal (junctional) premature beat", - "Atrial premature beat", - "Supraventricular premature or ectopic beat", - "Ventricular escape beat", - "Nodal (junctional) escape beat", - "Paced beat", - "Unclassifiable beat", - "Change in signal quality", - (char *)NULL, - "Isolated QRS-like artifact", - (char *)NULL, - "ST segment change", - "T-wave change", - "Systole", - "Diastole", - "Comment annotation", - "Measurement annotation", - "P-wave peak", - "Bundle branch block beat (unspecified)", - "(Non-captured) pacemaker artifact", - "T-wave peak", - "Rhythm change", - "U-wave peak", - "Beat not classified during learning", - "Ventricular flutter wave", - "Start of ventricular flutter/fibrillation", - "End of ventricular flutter/fibrillation", - "Atrial escape beat", - "Supraventricular escape beat", - "Link to external data (aux contains URL)", - "Non-conducted P-wave (blocked APC)", - "Fusion of paced and normal beat", - "Waveform onset", - "Waveform end", - "R-on-T premature ventricular contraction", - (char *)NULL, - (char *)NULL, - (char *)NULL, - (char *)NULL, - (char *)NULL, - (char *)NULL, - (char *)NULL, +FINT setannstr(int code, const char *string) +{ + if (set_def_string(astring, modified, code, string) == 0) + return (0); + wfdb_error("setannstr: illegal annotation code %d\n", + code > 0 ? code : -code); + return (-1); +} + +/* wfdb-x: the default descriptive strings (for tstring and tstring0) */ +#define DEFAULT_TSTRINGS \ + "", \ + "Normal beat", \ + "Left bundle branch block beat", \ + "Right bundle branch block beat", \ + "Aberrated atrial premature beat", \ + "Premature ventricular contraction", \ + "Fusion of ventricular and normal beat", \ + "Nodal (junctional) premature beat", \ + "Atrial premature beat", \ + "Supraventricular premature or ectopic beat", \ + "Ventricular escape beat", \ + "Nodal (junctional) escape beat", \ + "Paced beat", \ + "Unclassifiable beat", \ + "Change in signal quality", \ + (char *)NULL, \ + "Isolated QRS-like artifact", \ + (char *)NULL, \ + "ST segment change", \ + "T-wave change", \ + "Systole", \ + "Diastole", \ + "Comment annotation", \ + "Measurement annotation", \ + "P-wave peak", \ + "Bundle branch block beat (unspecified)", \ + "(Non-captured) pacemaker artifact", \ + "T-wave peak", \ + "Rhythm change", \ + "U-wave peak", \ + "Beat not classified during learning", \ + "Ventricular flutter wave", \ + "Start of ventricular flutter/fibrillation", \ + "End of ventricular flutter/fibrillation", \ + "Atrial escape beat", \ + "Supraventricular escape beat", \ + "Link to external data (aux contains URL)", \ + "Non-conducted P-wave (blocked APC)", \ + "Fusion of paced and normal beat", \ + "Waveform onset", \ + "Waveform end", \ + "R-on-T premature ventricular contraction", \ + (char *)NULL, \ + (char *)NULL, \ + (char *)NULL, \ + (char *)NULL, \ + (char *)NULL, \ + (char *)NULL, \ + (char *)NULL, \ (char *)NULL + +static char *tstring[ACMAX+1] = { /* descriptive strings for each code */ + DEFAULT_TSTRINGS }; +static char *const tstring0[ACMAX+1] = { DEFAULT_TSTRINGS }; /* wfdb-x */ FSTRING anndesc(int code) { @@ -969,23 +1070,11 @@ FSTRING anndesc(int code) FINT setanndesc(int code, const char *string) { - int mflag = 0; - - if (code > 0) mflag = 1; - else code = -code; - if (code <= ACMAX) { - if (string == NULL) string = ""; - if (tstring[code] == NULL || strcmp(tstring[code], string)) { - tstring[code] = NULL; - SSTRCPY(tstring[code], string); - if (mflag) modified[code] = 1; - } + if (set_def_string(tstring, modified, code, string) == 0) return (0); - } - else { - wfdb_error("setanndesc: illegal annotation code %d\n", code); - return (-1); - } + wfdb_error("setanndesc: illegal annotation code %d\n", + code > 0 ? code : -code); + return (-1); } @@ -1068,10 +1157,229 @@ FVOID iannclose(WFDB_Annotator n) } } +/* wfdb-x: in-process sorting of an output annotation file. sort_ann_file + rearranges the annotations in the (closed) annotation file fname of + annotator aname into the canonical order, with a result identical, byte for + byte, to that of the sortann program as the C library ran it ("sortann -r. + RECORD -a ANNOTATOR", see oannclose), but without looking up the header + of the record. As sortann does, it reads the file (with any time + resolution and annotation type definitions it contains), builds a list of + its annotations with sort_insert (insert_ann in app/sortann.c), and, unless + they were already in order, rewrites the file in MIT format, with the + time resolution of the input file (if any) and without the NOTQRS + annotations. It uses its own reader, writer, and definition table (the + library's annotators and tables are not changed). It returns 0 on + success, or -1 if the file cannot be read or written (or memory is + insufficient). */ + +struct sort_entry { /* an annotation in the list */ + WFDB_Annotation ann; + struct sort_entry *prev, *next; + struct sort_entry *allocated; /* the previously allocated entry */ +}; + +struct sort_list { + struct sort_entry head; /* its annotation is all zeros, prev NULL */ + struct sort_entry *last; /* the last entry (initially &head) */ + struct sort_entry *allocated; /* the last allocated entry */ + int in_order; /* 0 if an annotation was out of order */ +}; + +/* AFTER(a, b) is true if annotation a follows annotation b in the canonical + order; AFTER_OR_SAME(a, b) is true if it does so or they have the same + time, num, and chan. */ +#define AFTER(a, b) ((a)->time > (b)->time || \ + ((a)->time == (b)->time && (a)->num > (b)->num) || \ + ((a)->time == (b)->time && (a)->num == (b)->num && \ + (a)->chan > (b)->chan)) +#define AFTER_OR_SAME(a, b) ((a)->time > (b)->time || \ + ((a)->time == (b)->time && (a)->num > (b)->num) || \ + ((a)->time == (b)->time && (a)->num == (b)->num && \ + (a)->chan >= (b)->chan)) + +/* sort_insert inserts a copy of *pa into the list l as insert_ann does, + except in its corner case (wfdb-x: a fix): if *pa precedes all entries and + has the time, num, and chan of the annotation of the list head (all + zeros), insert_ann "replaces" the head, linking *pa before the first entry + but not to the head, so that *pa is not written (nor are annotations later + inserted after it); here the head is never compared with *pa, and *pa is + inserted at the beginning of the list. Entries are freed by sort_free + (insert_ann frees a replaced entry at once). It returns 0, or -1 if + memory is insufficient. */ +static int sort_insert(struct sort_list *l, const WFDB_Annotation *pa) +{ + struct sort_entry *ap, *newp; + unsigned char *p = NULL; + + if (pa->aux && (p = (unsigned char *)malloc(*(pa->aux)+2)) == NULL) + return (-1); + if ((newp = (struct sort_entry *)malloc(sizeof(struct sort_entry))) == + NULL) { + free(p); + return (-1); + } + newp->ann = *pa; + if (pa->aux) { + memcpy(p, pa->aux, *(pa->aux)+2); + newp->ann.aux = p; + } + newp->allocated = l->allocated; + l->allocated = newp; + if (l->last == &l->head || AFTER(pa, &l->last->ann)) { + /* this annotation is in order -- add to end of list */ + newp->prev = l->last; + l->last->next = newp; + l->last = newp; + newp->next = NULL; + return (0); + } + l->in_order = 0; + /* Find the correct location in the list for this annotation, searching + backwards from the end of the list. */ + for (ap = l->last; ap && ap != &l->head && !AFTER_OR_SAME(pa, &ap->ann); + ap = ap->prev) + ; + if (ap == NULL || ap == &l->head) { /* insert newp at the beginning of + the list (wfdb-x: also if the + search reached the head) */ + newp->prev = NULL; + newp->next = l->head.next; + (newp->next)->prev = l->head.next = newp; + } + else if (pa->time == ap->ann.time && pa->num == ap->ann.num && + pa->chan == ap->ann.chan) { /* replace ap by newp */ + if ((newp->prev = ap->prev) != NULL) (newp->prev)->next = newp; + if ((newp->next = ap->next) != NULL) (newp->next)->prev = newp; + else l->last = newp; + /* If ap is the first entry but its prev does not lead to the head, + insert_ann leaves the head pointing to it, and frees it (undefined + behavior, see docs/KNOWN_C_BEHAVIORS.md); here, as in the wfdb-x + sortann, the head is updated. */ + if (l->head.next == ap) + l->head.next = newp; + } + else { /* insert newp immediately after ap */ + newp->prev = ap; + newp->next = ap->next; + (newp->next)->prev = ap->next = newp; + } + return (0); +} + +/* sort_free frees all entries allocated by sort_insert for the list l. */ +static void sort_free(struct sort_list *l) +{ + struct sort_entry *ep; + + while ((ep = l->allocated) != NULL) { + l->allocated = ep->allocated; + free(ep->ann.aux); + free(ep); + } +} + +/* shell_quote (wfdb-x) returns a newly allocated copy of s quoted for the + POSIX shell ('...', each ' written as '\''), or NULL if memory is + insufficient. */ +static char *shell_quote(const char *s) +{ + size_t n = 3; + const char *p; + char *q, *r; + + for (p = s; *p; p++) + n += (*p == '\'') ? 4 : 1; + if ((q = r = (char *)malloc(n)) == NULL) + return (NULL); + *r++ = '\''; + for (p = s; *p; p++) { + if (*p == '\'') { + memcpy(r, "'\\''", 4); + r += 4; + } + else + *r++ = *p; + } + *r++ = '\''; + *r = '\0'; + return (q); +} + +static int sort_ann_file(char *fname, char *aname) +{ + char *astr[ACMAX+1], *tstr[ACMAX+1], mod[ACMAX+1]; + int a, ok, stat; + struct ann_defs defs; + struct iadata ia; + struct oadata oa; + struct sort_list list; + struct sort_entry *ep; + WFDB_Annotation annot; + + /* Read the file, starting from the default definition table. */ + for (a = 0; a <= ACMAX; a++) { + astr[a] = astring0[a]; + tstr[a] = tstring0[a]; + mod[a] = 0; + } + defs.astr = astr; + defs.tstr = tstr; + defs.mod = mod; + memset(&ia, 0, sizeof(ia)); + memset(&list, 0, sizeof(list)); + list.last = &list.head; + list.in_order = 1; + if ((ia.file = wfdb_fopen(fname, "rb")) == NULL) + return (-1); + ia.info.name = aname; + ia.info.stat = ann_file_format(&ia); + (void)read_ann_table(&ia, &defs); + /* As sortann (whose -f time is 0), skip annotations at negative times. */ + while ((stat = getann_ia(&ia, &annot)) == 0) + if (annot.time >= 0 && sort_insert(&list, &annot) < 0) + break; + (void)wfdb_fclose(ia.file); + ok = (stat == -1); /* -1: logical end of file */ + + /* Write the sorted annotations, unless they were already in order. */ + if (ok && !list.in_order) { + memset(&oa, 0, sizeof(oa)); + oa.info.name = aname; + oa.info.stat = WFDB_WRITE; + if ((oa.file = wfdb_fopen(fname, "wb")) == NULL) + ok = 0; + else { + for (ep = list.head.next; ep && ok; ep = ep->next) { + if (ep->ann.anntyp == NOTQRS) + continue; + if (!oa.table_written) { + oa.table_written = 1; + if (put_ann_table(&oa, 0, ia.afreq, &defs) < 0) + ok = 0; + } + if (ok && put_ann_word(&oa, 0, &ep->ann) < 0) + ok = 0; + } + wfdb_p16(0, oa.file); + if (wfdb_ferror(oa.file)) + ok = 0; + if (wfdb_fclose(oa.file)) + ok = 0; + } + } + + sort_free(&list); + for (a = 0; a <= ACMAX; a++) { /* free the definitions read */ + if (astr[a] != astring0[a]) free(astr[a]); + if (tstr[a] != tstring0[a]) free(tstr[a]); + } + return (ok ? 0 : -1); +} + /* oannclose: close output annotation file n */ FVOID oannclose(WFDB_Annotator n) { - int i, errflag; + int i, errflag, sorted; char *cmdbuf = NULL; struct oadata *oa; @@ -1100,30 +1408,50 @@ FVOID oannclose(WFDB_Annotator n) if (p) dosort = strtol(p, NULL, 10); if (dosort) { - if (system(NULL) != 0) { + /* wfdb-x: the file is sorted in this process (sort_ann_file); + the C library ran the sortann program, as legacy mode does. */ + int legacy = wfdbx_get_legacy(); + + if (!legacy || system(NULL) != 0) { wfdb_error( "Rearranging annotations for output annotator %s ...", oa->info.name); - /* The option '-r.' tells sortann to change its - WFDB path to the current directory (i.e, it - should only attempt to read the annotation file - that we just created, and should not look - elsewhere, regardless of the WFDB environment - variable.) - - Older versions of sortann interpret '-r.' as - equivalent to '-r', and will search for the - given annotation file within the WFDB path. */ - wfdb_asprintf(&cmdbuf, "sortann -r. %s -a %s", - oa->rname, oa->info.name); - if (cmdbuf && system(cmdbuf) == 0) { + if (!legacy) + sorted = (sort_ann_file(oa->fname, oa->info.name) == 0); + else { + /* The option '-r.' tells sortann to change its + WFDB path to the current directory (i.e, it + should only attempt to read the annotation file + that we just created, and should not look + elsewhere, regardless of the WFDB environment + variable.) + + Older versions of sortann interpret '-r.' as + equivalent to '-r', and will search for the + given annotation file within the WFDB path. */ + /* wfdb-x: sortann runs with --legacy (so that it also + behaves as the original program), and, with an output + directory, in that directory (where the file is) */ + char *od = wfdbx_get_output_dir(), *q = NULL; + + if (od && (q = shell_quote(od)) != NULL) + wfdb_asprintf(&cmdbuf, + "cd %s && sortann --legacy -r. %s -a %s", + q, oa->rname, oa->info.name); + else if (!od) + wfdb_asprintf(&cmdbuf, "sortann --legacy -r. %s -a %s", + oa->rname, oa->info.name); + SFREE(q); + sorted = (cmdbuf && system(cmdbuf) == 0); + SFREE(cmdbuf); + } + if (sorted) { wfdb_error("done!\n"); oa->out_of_order = 0; } else wfdb_error( "\nAnnotations still need to be rearranged.\n"); - SFREE(cmdbuf); if (annclose_error == 0) annclose_error = -6; } @@ -1138,6 +1466,7 @@ FVOID oannclose(WFDB_Annotator n) } SFREE(oa->info.name); SFREE(oa->rname); + SFREE(oa->fname); SFREE(oa); while (n < noaf-1) { oad[n] = oad[n+1]; diff --git a/lib/signal.c b/lib/signal.c index 901efc2..a1d739b 100644 --- a/lib/signal.c +++ b/lib/signal.c @@ -185,7 +185,15 @@ void example(void) #include "wfdblib.h" #include +#include #include +#ifdef _WIN32 /* wfdb-x: for file_id */ +#include +#include +#else +#include +#include +#endif #ifdef iAPX286 #define BROKEN_CC @@ -203,6 +211,78 @@ void example(void) #define FLAC__StreamDecoder struct dummy #endif +/* wfdb-x: legacy mode. By default, the library has no built-in signal + parameters (see docs/DIVERGENCES.md). In legacy mode (wfdbx_set_legacy(1), + or --legacy as the first argument of an application, see + wfdbx_legacy_args), it behaves exactly as the original WFDB library 10.7.0, + with its built-in defaults: WFDB_DEFFREQ for a header without a sampling + frequency, an ADC resolution inferred from the format or WFDB_DEFRES, + 1 sample per second in the time conversion functions, and WFDB_DEFGAIN + (and a baseline of 0 for invalid signal numbers) in the unit conversion + functions. It exists for compatibility only. */ +static int legacy_mode = 0; + +/* wfdbx_set_legacy (a wfdb-x addition) turns legacy mode on (if on is + nonzero) or off, and returns the previous setting (1: on, 0: off). */ +FINT wfdbx_set_legacy(int on) +{ + int previous = legacy_mode; + + legacy_mode = (on != 0); + return (previous); +} + +/* wfdbx_get_legacy (a wfdb-x addition) returns 1 if legacy mode is on, 0 + otherwise. */ +FINT wfdbx_get_legacy(void) +{ + return (legacy_mode); +} + +/* wfdbx_legacy_args (a wfdb-x addition) is called by the applications at the + beginning of main, as wfdbx_legacy_args(&argc, &argv). If the first + argument (argv[1]) is "--legacy", it removes it from the argument list (the + following arguments move up by one, argv[0] is unchanged, and argc is + decremented), turns legacy mode on, and returns 1; the application then + behaves exactly as the original one (options, outputs, messages, and exit + statuses). Otherwise, it changes nothing and returns 0. Only the first + argument is recognized. */ +FINT wfdbx_legacy_args(int *argc, char ***argv) +{ + char **v = *argv; + + wfdbx_exit_check_init(v[0]); /* (see wfdbio.c) */ + if (*argc < 2 || strcmp(v[1], "--legacy") != 0) + return (0); + v[1] = v[0]; + *argv = v + 1; + (*argc)--; + (void)wfdbx_set_legacy(1); + return (1); +} + +/* wfdb-x: files named in a header. In normal mode, the signal files named in + a header, and the segments (and layout header) named in the header of a + multi-segment record, are in the directory of that header (on disk or at + its URL), or below it: "a.dat" is
/a.dat; a name that is + absolute, begins with '~', is a URL, or contains a ".." component is + refused (except "~" as a segment, a null segment, and "-" as a signal file, + the standard input). Nothing is searched (the C library searched the WFDB + path, whose first entries were the directories of the files opened + before). Decided by Reza Sameni, Oct 2026: a search can silently open the + wrong file, and a header must not make the library read other local files + or contact other servers. The original behavior remains available with + --legacy (see docs/DIVERGENCES.md). */ +static char *hdr_dir; /* directory of the header read last by + readheader (see wfdbx_dirname; NULL if + unknown, i.e., for the standard input) */ +static char *msrec_dir, *msrec_name; + /* directory and name of the header of the + current multi-segment record (msrec_name + is NULL if there is none) */ +static int user_segments; /* 1 while setmsheader reads the headers of + the segments it was given */ + /* Shared local data */ /* These variables are set by readheader, and contain information about the @@ -766,6 +846,29 @@ static void read_edf_str(char *buf, int size, WFDB_FILE *ifile) } } +/* wfdb-x: open_header opens the header (of type s: "hea", or NULL for a file + name) of a record, or of a segment named in the header of the current + multi-segment record, and notes its directory (see hdr_dir). */ +static WFDB_FILE *open_header(const char *s, const char *record) +{ + WFDB_FILE *f; + + if (legacy_mode) + return (wfdb_open(s, record, WFDB_READ)); + if (!in_msrec) { + SFREE(msrec_dir); + SFREE(msrec_name); + } + if (in_msrec && msrec_name && !user_segments) + f = wfdbx_open_in(msrec_dir, s, record, msrec_name); + else + f = wfdb_open(s, record, WFDB_READ); + SFREE(hdr_dir); + if (f && f->fp != stdin) + hdr_dir = wfdbx_dirname(wfdbfile(NULL, NULL)); + return (f); +} + /* get header information from an EDF file */ static int edfparse(WFDB_FILE *ifile) { @@ -963,7 +1066,7 @@ static int readheader(const char *record) while (q > record && *q != '.' && *q != '/' && *q != ':' && *q != '\\') q--; if (*q == '.') { - if ((hheader = wfdb_open(NULL, record, WFDB_READ)) == NULL) { + if ((hheader = open_header(NULL, record)) == NULL) { wfdb_error("init: can't open %s\n", record); return (-1); } @@ -972,7 +1075,7 @@ static int readheader(const char *record) } /* Otherwise, assume the file name is record.hea. */ - else if ((hheader = wfdb_open("hea", record, WFDB_READ)) == NULL) { + else if ((hheader = open_header("hea", record)) == NULL) { wfdb_error("init: can't open header for record %s\n", record); return (-1); } @@ -1075,7 +1178,10 @@ static int readheader(const char *record) else ffreq = f; } - else if (ffreq == (WFDB_Frequency)0.) + /* wfdb-x: if the header does not specify the sampling frequency (and the + caller has not set one with setsampfreq), it remains unspecified (0); + the C library assumed WFDB_DEFFREQ (250 Hz), as legacy mode does. */ + else if (legacy_mode && ffreq == (WFDB_Frequency)0.) ffreq = WFDB_DEFFREQ; /* Set the sampling rate to the frame rate for now. This may be @@ -1128,6 +1234,12 @@ static int readheader(const char *record) /* Special processing for master header of a multi-segment record. */ if (segments && !in_msrec) { + /* wfdb-x: the segments are in the directory of this header */ + if (!legacy_mode) { + SFREE(msrec_dir); + if (hdr_dir) SSTRCPY(msrec_dir, hdr_dir); + SSTRCPY(msrec_name, record); + } msbtime = btime; msbdate = bdate; msnsamples = nsamples; @@ -1290,12 +1402,12 @@ static int readheader(const char *record) else hs->info.units = NULL; - /* Determine the ADC resolution in bits. If this number is - missing and cannot be inferred from the format, the default - value (from wfdb.h) is filled in. */ + /* Determine the ADC resolution in bits. wfdb-x: if this number is + missing, it is unspecified (0); the C library inferred it from the + format or assumed WFDB_DEFRES (12 bits), as legacy mode does. */ if (p = strtok((char *)NULL, sep)) i = (unsigned)strtol(p, NULL, 10); - else switch (hs->info.fmt) { + else if (legacy_mode) switch (hs->info.fmt) { case 80: i = 8; break; case 160: i = 16; break; case 212: i = 12; break; @@ -1303,6 +1415,8 @@ static int readheader(const char *record) case 311: i = 10; break; default: i = WFDB_DEFRES; break; } + else + i = 0; hs->info.adcres = i; /* Determine the ADC zero (assumed to be zero if missing). */ @@ -3098,7 +3212,10 @@ FINT isigopen(char *record, WFDB_Siginfo *siarray, int nsig) if (hs->info.fmt == 0) ig->fp = NULL; /* Don't open a file for a null signal. */ else { - ig->fp = wfdb_open(hs->info.fname, (char *)NULL, WFDB_READ); + /* wfdb-x: in the directory of the header */ + ig->fp = legacy_mode ? + wfdb_open(hs->info.fname, (char *)NULL, WFDB_READ) : + wfdbx_open_in(hdr_dir, NULL, hs->info.fname, record); /* Skip this group if the signal file can't be opened. */ if (ig->fp == NULL) { SFREE(ig->buf); @@ -3200,6 +3317,164 @@ FINT isigopen(char *record, WFDB_Siginfo *siarray, int nsig) return (s); } +/* wfdb-x: an output signal file is never one of the input files. In normal + mode, openosig refuses to create (and so to truncate) an output signal file + that is the same file as a signal file of the open input record: a signal + file open for input, or a signal file of any segment of the open + multi-segment record (read from the segment's header; segments are opened + later, as they are reached). "The same file" is decided by the files + themselves, not by their names: device and inode numbers on POSIX, volume + serial number and file index on MS-Windows. Decided by Reza Sameni, Oct + 2026: e.g., "xform -i vlay -o fmt16", fmt16 being a segment of vlay, + truncated fmt16.dat while reading it, with a platform-dependent result and + the input destroyed. Legacy mode: no check (the original behavior). */ +struct file_id { +#ifdef _WIN32 + DWORD vol, hi, lo; +#else + dev_t dev; + ino_t ino; +#endif +}; + +/* file_id gets the identity of the open local file fp; it returns 1 on + success, 0 otherwise. */ +static int file_id(FILE *fp, struct file_id *id) +{ +#ifdef _WIN32 + BY_HANDLE_FILE_INFORMATION info; + HANDLE h = (HANDLE)_get_osfhandle(_fileno(fp)); + + if (h == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(h, &info)) + return (0); + id->vol = info.dwVolumeSerialNumber; + id->hi = info.nFileIndexHigh; + id->lo = info.nFileIndexLow; +#else + struct stat st; + + if (fstat(fileno(fp), &st) != 0) + return (0); + id->dev = st.st_dev; + id->ino = st.st_ino; +#endif + return (1); +} + +static int same_id(const struct file_id *a, const struct file_id *b) +{ +#ifdef _WIN32 + return (a->vol == b->vol && a->hi == b->hi && a->lo == b->lo); +#else + return (a->dev == b->dev && a->ino == b->ino); +#endif +} + +/* is_file (WFDB_FILE *f, struct file_id *id) returns 1 if f is a local file + (not the standard input, not a URL) whose identity is id. */ +static int is_file(WFDB_FILE *f, const struct file_id *id) +{ + struct file_id fid; + + return (f && f->type == WFDB_LOCAL && f->fp && f->fp != stdin && + file_id(f->fp, &fid) && same_id(&fid, id)); +} + +static int is_url_name(const char *s) +{ + return (strncmp(s, "http://", 7) == 0 || strncmp(s, "https://", 8) == 0); +} + +/* segment_has_file returns 1 if one of the signal files named in the header + of segment seg (of the open multi-segment record) is the file id. The + header and the signal files are located as isigopen locates them in normal + mode; a header or signal file that cannot be found (or is at a URL) is not + the file id. No message is printed. */ +static int segment_has_file(const char *seg, const struct file_id *id) +{ + WFDB_FILE *hf, *sf; + char *buf = NULL, *dir = NULL, *p, *q; + size_t bufsize = 0; + int found = 0, nsig = -1; + const char *sep = " \t\n\r"; + + if (strcmp(seg, "~") == 0 || !wfdbx_in_header_dir(seg) || + (msrec_dir && is_url_name(msrec_dir))) + return (0); + if ((hf = (msrec_name ? wfdbx_open_in(msrec_dir, "hea", seg, msrec_name) : + wfdb_open("hea", seg, WFDB_READ))) == NULL) + return (0); + if (hf->type == WFDB_LOCAL && hf->fp != stdin) + dir = wfdbx_dirname(wfdbfile(NULL, NULL)); + while (!found && nsig != 0 && wfdb_getline(&buf, &bufsize, hf)) { + if ((p = strtok(buf, sep)) == NULL || *p == '#') + continue; + if (nsig < 0) { /* the record line */ + if (strchr(p, '/') || /* not a valid segment */ + (q = strtok(NULL, sep)) == NULL || + (nsig = strtol(q, NULL, 10)) <= 0) + break; + continue; + } + nsig--; /* a signal line: file name, format, ... */ + if ((q = strtok(NULL, sep)) == NULL || strtol(q, NULL, 10) == 0 || + strcmp(p, "-") == 0 || !wfdbx_in_header_dir(p) || + dir == NULL || is_url_name(dir)) + continue; + if ((sf = wfdbx_open_in(dir, NULL, p, seg)) != NULL) { + found = is_file(sf, id); + (void)wfdb_fclose(sf); + } + } + SFREE(buf); + SFREE(dir); + (void)wfdb_fclose(hf); + return (found); +} + +/* is_input_file returns 1 if the output signal file fname (as named in an + output signal specification, created by wfdb_open(fname, NULL, + WFDB_WRITE)) exists and is one of the signal files of the open input + record (see above). */ +static int is_input_file(const char *fname) +{ + char *path = wfdbx_output_file(fname); + struct file_id id; + int found = 0, ok; + WFDB_Group g; + WFDB_Seginfo *sp; +#ifdef _WIN32 + FILE *fp; +#else + struct stat st; +#endif + + if (path == NULL) + return (0); +#ifdef _WIN32 + if ((fp = fopen(path, "rb")) != NULL) { + ok = file_id(fp, &id); + fclose(fp); + } + else + ok = 0; +#else + if ((ok = (stat(path, &st) == 0))) { + id.dev = st.st_dev; + id.ino = st.st_ino; + } +#endif + SFREE(path); + if (!ok) + return (0); /* it does not exist: it is not an input file */ + for (g = 0; g < nigroup && !found; g++) + found = igd[g] && is_file(igd[g]->fp, &id); + if (in_msrec && segarray) + for (sp = segarray; sp <= segend && !found; sp++) + found = segment_has_file(sp->recname, &id); + return (found); +} + static int openosig(const char *func, WFDB_Siginfo *si_out, const WFDB_Siginfo *si_in, unsigned int nsig) { @@ -3269,6 +3544,14 @@ static int openosig(const char *func, WFDB_Siginfo *si_out, os->info.fmt = 16; } if (os->info.fmt != 0) { + /* wfdb-x: never an input file (see is_input_file) */ + if (!legacy_mode && is_input_file(os->info.fname)) { + wfdb_error("%s: output file %s is also an input file\n", + func, os->info.fname); + SFREE(og->buf); + osigclose(); + return (-3); + } /* An error in opening an output file is fatal. */ og->fp = wfdb_open(os->info.fname,(char *)NULL, WFDB_WRITE); if (og->fp == NULL) { @@ -3469,7 +3752,10 @@ FINT setifreq(WFDB_Frequency f) if (g <= 0.0) { ifreq = 0.0; - wfdb_error("setifreq: no open input record\n"); + if (nisig > 0 && !legacy_mode) /* wfdb-x: the frequency is unspecified */ + wfdb_error("setifreq: sampling frequency is not specified\n"); + else + wfdb_error("setifreq: no open input record\n"); return (-1); } if (f > 0.0) { @@ -3827,6 +4113,22 @@ FINT setobsize(int n) return (obsize = n); } +/* wfdb-x: outside legacy mode, a name written into the header of record is + written relative to the header's directory (where the reader looks for + it): the directory part of record is removed from name when name begins + with it. */ +static const char *hea_name(const char *record, const char *name) +{ + const char *q; + size_t n; + + if (wfdbx_get_legacy() || name == NULL || + (q = strrchr(record, '/')) == NULL) + return (name); + n = q - record + 1; + return (strncmp(name, record, n) == 0 ? name + n : name); +} + FINT newheader(char *record) { int stat; @@ -3875,7 +4177,8 @@ FINT setheader(char *record, const WFDB_Siginfo *siarray, unsigned int nsig) } /* Write the general information line. */ - (void)wfdb_fprintf(oheader, "%s %d %.12g", record, nsig, ffreq); + (void)wfdb_fprintf(oheader, "%s %d %.12g", hea_name(record, record), nsig, + ffreq); if ((cfreq > 0.0 && cfreq != ffreq) || bcount != 0.0) { (void)wfdb_fprintf(oheader, "/%.12g", cfreq); if (bcount != 0.0) @@ -3898,7 +4201,8 @@ FINT setheader(char *record, const WFDB_Siginfo *siarray, unsigned int nsig) /* Write a signal specification line for each signal. */ for (s = 0; s < nsig; s++) { - (void)wfdb_fprintf(oheader, "%s %d", siarray[s].fname, siarray[s].fmt); + (void)wfdb_fprintf(oheader, "%s %d", hea_name(record, siarray[s].fname), + siarray[s].fmt); if (siarray[s].spf > 1) (void)wfdb_fprintf(oheader, "x%d", siarray[s].spf); if (osd && osd[s]->skew) @@ -3971,7 +4275,9 @@ FINT setmsheader(char *record, char **segment_name, unsigned int nsegments) } in_msrec = 1; nsamples = 0; + user_segments = 1; /* wfdb-x: names given by the caller */ n = readheader(segment_name[i]); + user_segments = 0; in_msrec = old_in_msrec; if (n < 0) { wfdb_error("setmsheader: can't read segment %s header\n", @@ -4022,7 +4328,8 @@ FINT setmsheader(char *record, char **segment_name, unsigned int nsegments) } /* Write the first line of the master header. */ - (void)wfdb_fprintf(oheader,"%s/%u %d %.12g", record, nsegments, nsig, msfreq); + (void)wfdb_fprintf(oheader,"%s/%u %d %.12g", hea_name(record, record), + nsegments, nsig, msfreq); if ((mscfreq > 0.0 && mscfreq != msfreq) || msbcount != 0.0) { (void)wfdb_fprintf(oheader, "/%.12g", mscfreq); if (msbcount != 0.0) @@ -4044,7 +4351,7 @@ FINT setmsheader(char *record, char **segment_name, unsigned int nsegments) /* Write a line for each segment. */ for (i = 0; i < nsegments; i++) (void)wfdb_fprintf(oheader, "%s %"WFDB_Pd_TIME"\r\n", - segment_name[i], ns[i]); + hea_name(record, segment_name[i]), ns[i]); SFREE(ns); return (0); @@ -4325,13 +4632,42 @@ static char *ftimstr(WFDB_Time t, WFDB_Frequency f) return (p); } +/* wfdb-x: the time conversion functions need a sampling frequency; the C + library used 1 sample per second when none was specified. Without one, + timstr and mstimstr report an error and return the time as a sample number + ("s" followed by the number, a valid strtim argument), and strtim reports + an error and returns 0 for time strings that need a frequency. In legacy + mode, they use 1 sample per second, as the C library did. */ +static char nofreq_string[24]; + +static char *nofreq_timstr(const char *fn, WFDB_Time t) +{ + wfdb_error("%s: sampling frequency is not specified\n", fn); + (void)sprintf(nofreq_string, "s%"WFDB_Pd_TIME, t < 0 ? -t : t); + return (nofreq_string); +} + +/* wfdbx_time_needs_freq (a wfdb-x addition) returns 1 if strtim needs a + sampling frequency to convert the string, 0 otherwise. */ +FINT wfdbx_time_needs_freq(const char *string) +{ + while (*string==' ' || *string=='\t' || *string=='\n' || *string=='\r') + string++; + switch (*string) { + case 'e': case 'i': case 'o': case 's': return (0); + case 'c': return (cfreq > 0.); + default: return (1); + } +} + FSTRING timstr(WFDB_Time t) { double f; if (ifreq > 0.) f = ifreq; else if (sfreq > 0.) f = sfreq; - else f = 1.0; + else if (legacy_mode) f = 1.0; + else return (nofreq_timstr("timstr", t)); return ftimstr(t, f); } @@ -4398,7 +4734,8 @@ FSTRING mstimstr(WFDB_Time t) if (ifreq > 0.) f = ifreq; else if (sfreq > 0.) f = sfreq; - else f = 1.0; + else if (legacy_mode) f = 1.0; + else return (nofreq_timstr("mstimstr", t)); return fmstimstr(t, f); } @@ -4475,7 +4812,11 @@ FSITIME strtim(const char *string) if (ifreq > 0.) f = ifreq; else if (sfreq > 0.) f = sfreq; - else f = 1.0; + else if (!legacy_mode && wfdbx_time_needs_freq(string)) { + wfdb_error("strtim: sampling frequency is not specified\n"); + return (0L); + } + else f = 1.0; /* legacy mode; otherwise not used */ return fstrtim(string, f); } @@ -4533,12 +4874,33 @@ FDATE strdat(const char *string) return (date); } +/* wfdb-x: the unit conversion functions need the gain of the signal; the C + library assumed WFDB_DEFGAIN (200 adu per physical unit) for uncalibrated + signals (gain 0) and invalid signal numbers (with a baseline of 0), as + legacy mode does. Otherwise, they report an error and return + WFDB_INVALID_SAMPLE (aduphys returns NaN). */ +static WFDB_Gain conversion_gain(const char *fn, WFDB_Signal s) +{ + if (legacy_mode) { + WFDB_Gain g = (s < nvsig) ? vsd[s]->info.gain : WFDB_DEFGAIN; + + return ((g == 0.) ? WFDB_DEFGAIN : g); + } + if (s >= nvsig) { + wfdb_error("%s: invalid signal number %u\n", fn, s); + return (0.); + } + if (vsd[s]->info.gain == 0.) + wfdb_error("%s: signal %u is uncalibrated\n", fn, s); + return (vsd[s]->info.gain); +} + FINT adumuv(WFDB_Signal s, WFDB_Sample a) { double x; - WFDB_Gain g = (s < nvsig) ? vsd[s]->info.gain : WFDB_DEFGAIN; + WFDB_Gain g = conversion_gain("adumuv", s); - if (g == 0.) g = WFDB_DEFGAIN; + if (g == 0.) return (WFDB_INVALID_SAMPLE); x = a*1000./g; if (x >= 0.0) return ((int)(x + 0.5)); @@ -4549,9 +4911,9 @@ FINT adumuv(WFDB_Signal s, WFDB_Sample a) FSAMPLE muvadu(WFDB_Signal s, int v) { double x; - WFDB_Gain g = (s < nvsig) ? vsd[s]->info.gain : WFDB_DEFGAIN; + WFDB_Gain g = conversion_gain("muvadu", s); - if (g == 0.) g = WFDB_DEFGAIN; + if (g == 0.) return (WFDB_INVALID_SAMPLE); x = g*v*0.001; if (x >= 0.0) return ((int)(x + 0.5)); @@ -4562,34 +4924,20 @@ FSAMPLE muvadu(WFDB_Signal s, int v) FDOUBLE aduphys(WFDB_Signal s, WFDB_Sample a) { double b; - WFDB_Gain g; + WFDB_Gain g = conversion_gain("aduphys", s); - if (s < nvsig) { - b = vsd[s]->info.baseline; - g = vsd[s]->info.gain; - if (g == 0.) g = WFDB_DEFGAIN; - } - else { - b = 0; - g = WFDB_DEFGAIN; - } + if (g == 0.) return (NAN); + b = (s < nvsig) ? vsd[s]->info.baseline : 0; /* 0: legacy mode */ return ((a - b)/g); } FSAMPLE physadu(WFDB_Signal s, double v) { int b; - WFDB_Gain g; + WFDB_Gain g = conversion_gain("physadu", s); - if (s < nvsig) { - b = vsd[s]->info.baseline; - g = vsd[s]->info.gain; - if (g == 0.) g = WFDB_DEFGAIN; - } - else { - b = 0; - g = WFDB_DEFGAIN; - } + if (g == 0.) return (WFDB_INVALID_SAMPLE); + b = (s < nvsig) ? vsd[s]->info.baseline : 0; /* 0: legacy mode */ v *= g; if (v >= 0) return ((int)(v + 0.5) + b); @@ -4689,6 +5037,9 @@ void wfdb_sigclose(void) sfreq = ifreq = ffreq = (WFDB_Frequency)0; pdays = (WFDB_Date)-1; segments = in_msrec = skewmax = 0; + SFREE(msrec_dir); /* wfdb-x */ + SFREE(msrec_name); + SFREE(hdr_dir); if (dsbuf) { SFREE(dsbuf); dsbi = -1; diff --git a/lib/wfdb.h0 b/lib/wfdb.h0 index 271babb..984099e 100644 --- a/lib/wfdb.h0 +++ b/lib/wfdb.h0 @@ -464,6 +464,20 @@ extern FINT wfdb_setannpos(int code, int newval); extern FSTRING timstr(WFDB_Time t); extern FSTRING mstimstr(WFDB_Time t); extern FSITIME strtim(const char *time_string); +extern FINT wfdbx_time_needs_freq(const char *time_string); /* wfdb-x */ +extern FINT wfdbx_set_legacy(int on); /* wfdb-x */ +extern FINT wfdbx_get_legacy(void); /* wfdb-x */ +extern FINT wfdbx_legacy_args(int *argc, char ***argv); /* wfdb-x */ +extern FINT wfdbx_write_errors(void); /* wfdb-x */ +extern FVOID wfdbx_note_write_error(void); /* wfdb-x */ +extern FINT wfdbx_fclose_output(void *fp, const char *pname, + const char *name); /* wfdb-x */ +extern FSTRING wfdbx_set_input_dir(const char *dir); /* wfdb-x */ +extern FSTRING wfdbx_get_input_dir(void); /* wfdb-x */ +extern FSTRING wfdbx_set_output_dir(const char *dir); /* wfdb-x */ +extern FSTRING wfdbx_get_output_dir(void); /* wfdb-x */ +extern FINT wfdbx_set_network(int on); /* wfdb-x */ +extern FINT wfdbx_get_network(void); /* wfdb-x */ extern FSTRING datstr(WFDB_Date d); extern FDATE strdat(const char *date_string); extern FINT adumuv(WFDB_Signal s, WFDB_Sample a); @@ -538,11 +552,17 @@ extern FINT annopen(), isigopen(), osigopen(), wfdbinit(), findsig(), getspf(), adumuv(), newheader(), setheader(), setmsheader(), getseginfo(), wfdbputprolog(), setsampfreq(), setbasetime(), putinfo(), setinfo(), setibsize(), setobsize(), calopen(), getcal(), putcal(), newcal(), - wfdbgetskew(), sample_valid(), wfdb_me_fatal(); + wfdbgetskew(), sample_valid(), wfdb_me_fatal(), + wfdbx_time_needs_freq(), wfdbx_set_legacy(), wfdbx_get_legacy(), + wfdbx_legacy_args(), wfdbx_set_network(), wfdbx_get_network(), + wfdbx_write_errors(), wfdbx_fclose_output(); /* wfdb-x */ +extern FVOID wfdbx_note_write_error(); /* wfdb-x */ extern FLONGINT wfdbgetstart(); extern FSAMPLE muvadu(), physadu(), sample(); extern FSTRING ecgstr(), annstr(), anndesc(), timstr(), mstimstr(), - datstr(), getwfdb(), getinfo(), wfdberror(), wfdbfile(); + datstr(), getwfdb(), getinfo(), wfdberror(), wfdbfile(), + wfdbx_set_input_dir(), wfdbx_get_input_dir(), wfdbx_set_output_dir(), + wfdbx_get_output_dir(); /* wfdb-x */ extern FSITIME strtim(), tnextvec(); extern FDATE strdat(); extern FVOID setafreq(), setgvmode(), wfdb_freeinfo(), wfdbquit(), wfdbquiet(), diff --git a/lib/wfdbinit.c b/lib/wfdbinit.c index 41a154d..f1eac11 100644 --- a/lib/wfdbinit.c +++ b/lib/wfdbinit.c @@ -46,7 +46,11 @@ FVOID wfdbquit(void) wfdb_anclose(); /* close annotation files, reset variables */ wfdb_oinfoclose(); /* close info file */ wfdb_sigclose(); /* close signals, reset variables */ - resetwfdb(); /* restore the WFDB path */ + /* wfdb-x: in normal mode, the input directory is left as it is (in the + C library, resetwfdb restored the string returned by getwfdb, but + not the list of directories searched) */ + if (wfdbx_get_legacy()) + resetwfdb(); /* restore the WFDB path */ wfdb_sampquit(); /* release sample data buffer */ wfdb_freeinfo(); /* release info strings */ } diff --git a/lib/wfdbio.c b/lib/wfdbio.c index 10644e2..88e8104 100644 --- a/lib/wfdbio.c +++ b/lib/wfdbio.c @@ -38,6 +38,12 @@ This file contains definitions of the following WFDB library functions: wfdberror [4.5] (returns the most recent WFDB library error message) wfdbfile [4.3] (returns the complete pathname of a WFDB file) wfdbmemerr [10.4.6] (set behavior on memory errors) + wfdbx_set_input_dir (wfdb-x: sets the directory of input files) + wfdbx_get_input_dir (wfdb-x: returns the directory of input files) + wfdbx_set_output_dir (wfdb-x: sets the directory for output files) + wfdbx_get_output_dir (wfdb-x: returns the directory for output files) + wfdbx_set_network (wfdb-x: allows or forbids network access) + wfdbx_get_network (wfdb-x: returns 1 if network access is allowed) These functions expose config strings needed by the WFDB Toolkit for Matlab: wfdbversion [10.4.20] (return the string defined by VERSION) @@ -154,10 +160,25 @@ may not be available in certain older C libraries (e.g., UNIX version 7 and BSD 4.2). This file includes a portable implementation of strtok(), which can be obtained if necessary by defining the symbol NOSTRTOK when compiling this module. + +wfdb-x: files are found by explicit names instead of a search of the WFDB +path: a relative name is read under the input directory and written under the +output directory (wfdbx_set_input_dir, wfdbx_set_output_dir; by default, the +current directory), and an absolute name, a ~ name or a URL is used as given; +nothing else is searched. A URL is opened only if network access was allowed +(wfdbx_set_network or WFDBX_NETWORK); it is then read in full by the curl +program. Decided by Reza Sameni, Oct 2026: a search can silently open the +wrong file (a stale copy earlier in the path) and fails in ways nobody +notices, and the network must not be used without being asked for. The +original behavior remains available with --legacy (see docs/DIVERGENCES.md). */ #include "wfdblib.h" #include +#ifndef _WIN32 +#include /* wfdb-x: for expand_home */ +#include +#endif /* WFDB library functions */ @@ -177,15 +198,45 @@ static const char *wfdb_getiwfdb(char **p); /* resetwfdb is called by wfdbquit, and can be called within an application, to restore the WFDB path to the value that was returned by the first call -to getwfdb (or NULL if getwfdb was not called). */ +to getwfdb (or NULL if getwfdb was not called). + +wfdb-x: in normal mode, these functions set and return the input directory +(see wfdbx_set_input_dir) instead of the WFDB path. setwfdb(p): p, split +into components as the WFDB path is, must contain at most one non-empty +component, which becomes the input directory (none: the default); with more, +setwfdb reports an error and changes nothing. getwfdb returns the input +directory, or "." if none is set. resetwfdb restores the input directory +set by the last setwfdb, if any (wfdbquit does not call it in normal mode). +The WFDB environment variable, @FILE paths, %r and wfdb_addtopath are used +only in legacy mode. */ + +/* wfdb-x: the input directory (see wfdbx_set_input_dir) and the values used + by setwfdb and resetwfdb in normal mode. */ +static char *input_dir, *previous_input_dir, *env_input_dir; +static char *setwfdb_input_dir; /* the input directory set by setwfdb */ +static int input_dir_init, setwfdb_called; +static void init_input_dir(void); +static void put_input_dir(const char *dir); +static int setwfdb_normal(const char *p); FVOID resetwfdb(void) { + /* wfdb-x: the input directory set by the last setwfdb, if any */ + if (!wfdbx_get_legacy()) { + if (setwfdb_called) + put_input_dir(setwfdb_input_dir); + return; + } SSTRCPY(wfdbpath, wfdbpath_init); } FSTRING getwfdb(void) { + /* wfdb-x: the input directory, or "." if none is set */ + if (!wfdbx_get_legacy()) { + init_input_dir(); + return (input_dir ? input_dir : "."); + } if (wfdbpath == NULL) { const char *p = getenv("WFDB"); @@ -204,6 +255,11 @@ FVOID setwfdb(const char *p) { void wfdb_export_config(void); + /* wfdb-x: the path names a single input directory */ + if (!wfdbx_get_legacy()) { + (void)setwfdb_normal(p); + return; + } if (p == NULL && (p = getenv("WFDB")) == NULL) p = DEFWFDB; SSTRCPY(wfdbpath, p); wfdb_export_config(); @@ -231,6 +287,266 @@ FVOID wfdbverbose(void) static char *wfdb_filename; +/* wfdb-x: the settings that locate files (normal mode). Each is read from its + environment variable (WFDBX_INPUT_DIR, WFDBX_OUTPUT_DIR, WFDBX_NETWORK) + when it is first needed; a call of its wfdbx_set_ function overrides it, + and a NULL or empty value (a negative one for the network) restores the + default, i.e., the value of the environment variable, if set. */ +static char *output_dir, *previous_output_dir, *env_output_dir; +static int output_dir_init; +static int network = -1, env_network; /* network: -1 until first needed */ + +/* wfdb-x: is_absolute_path returns 1 if s is an absolute local path: on POSIX + systems, a path beginning with '/'; on MS-Windows, one beginning with + 'X:\', 'X:/', '\\', or '/'. */ +static int is_absolute_path(const char *s) +{ +#ifdef _WIN32 + if (((*s >= 'A' && *s <= 'Z') || (*s >= 'a' && *s <= 'z')) && + s[1] == ':' && (s[2] == '\\' || s[2] == '/')) + return (1); + if (s[0] == '\\' && s[1] == '\\') + return (1); +#endif + return (*s == '/'); +} + +/* wfdb-x: is_url returns 1 if s is an http:// or https:// URL. */ +static int is_url(const char *s) +{ + return (strncmp(s, "http://", 7) == 0 || strncmp(s, "https://", 8) == 0); +} + +/* wfdb-x: is_dsep returns 1 if c is a directory separator ('/', and on + MS-Windows also '\'). */ +static int is_dsep(int c) +{ +#ifdef _WIN32 + if (c == '\\') return (1); +#endif + return (c == '/'); +} + +static void init_input_dir(void) +{ + const char *p; + static int wfdb_warned; + + if (!input_dir_init) { + input_dir_init = 1; + if ((p = getenv("WFDBX_INPUT_DIR")) != NULL && *p) { + SSTRCPY(env_input_dir, p); + SSTRCPY(input_dir, p); + } + } + if (!wfdb_warned && !wfdbx_get_legacy()) { + wfdb_warned = 1; + if ((p = getenv("WFDB")) != NULL && *p) + wfdb_error("WFDB-x: the WFDB environment variable is ignored; set " + "WFDBX_INPUT_DIR instead (or use legacy mode)\n"); + } +} + +/* put_input_dir replaces the input directory (the default if dir is NULL or + empty), leaving previous_input_dir as it is. */ +static void put_input_dir(const char *dir) +{ + char *p = NULL; + + init_input_dir(); + if (dir && *dir) SSTRCPY(p, dir); + else if (env_input_dir) SSTRCPY(p, env_input_dir); + SFREE(input_dir); + input_dir = p; +} + +/* wfdbx_set_input_dir (a wfdb-x addition) sets the input directory: in normal + mode, wfdb_open reads a file named by a relative name as DIR/NAME. DIR may + be a local directory (relative to the current directory, or beginning with + ~) or the URL of a directory. NULL or an empty string restores the default + (the value of WFDBX_INPUT_DIR if it is set, otherwise none: names are used + as given, relative to the current directory). It returns the previous + value (NULL if none), which remains valid until the next call. */ +FSTRING wfdbx_set_input_dir(const char *dir) +{ + init_input_dir(); + SFREE(previous_input_dir); + previous_input_dir = input_dir; + input_dir = NULL; + put_input_dir(dir); + return (previous_input_dir); +} + +/* wfdbx_get_input_dir (a wfdb-x addition) returns the input directory, or + NULL if none is set. */ +FSTRING wfdbx_get_input_dir(void) +{ + init_input_dir(); + return (input_dir); +} + +static const char output_url_error[] = + "wfdbx_set_output_dir: the output directory must be a local directory\n"; + +static void init_output_dir(void) +{ + const char *p; + + if (output_dir_init) return; + output_dir_init = 1; + if ((p = getenv("WFDBX_OUTPUT_DIR")) != NULL && *p) { + if (is_url(p)) + wfdb_error(output_url_error); + else { + SSTRCPY(env_output_dir, p); + SSTRCPY(output_dir, p); + } + } +} + +/* wfdbx_set_output_dir (a wfdb-x addition) sets the output directory, in + which output files are created: wfdb_open, in WFDB_WRITE or WFDB_APPEND + mode, then creates a file named by a relative name NAME (the record name and + file type, as before) as DIR/NAME (in legacy mode, any name). NULL or an + empty string restores the default (the value of WFDBX_OUTPUT_DIR if it is + set, otherwise none: files are created as named). It returns the previous + value (NULL if none), which remains valid until the next call. A URL is + refused: the setting is unchanged, and the current value is returned. */ +FSTRING wfdbx_set_output_dir(const char *dir) +{ + init_output_dir(); + if (dir && is_url(dir)) { + wfdb_error(output_url_error); + return (output_dir); + } + SFREE(previous_output_dir); + previous_output_dir = output_dir; + output_dir = NULL; + if (dir && *dir) + SSTRCPY(output_dir, dir); + else if (env_output_dir) + SSTRCPY(output_dir, env_output_dir); + return (previous_output_dir); +} + +/* wfdbx_get_output_dir (a wfdb-x addition) returns the output directory, or + NULL if none is set. */ +FSTRING wfdbx_get_output_dir(void) +{ + init_output_dir(); + return (output_dir); +} + +/* wfdbx_set_network (a wfdb-x addition) allows (on > 0) or forbids (on == 0) + network access, i.e., opening URLs; a negative value restores the default + (allowed if the environment variable WFDBX_NETWORK is set to a non-zero + number, otherwise forbidden). It returns the previous setting (1 or 0). */ +FINT wfdbx_set_network(int on) +{ + int previous = wfdbx_get_network(); + + network = (on < 0) ? env_network : (on != 0); + return (previous); +} + +/* wfdbx_get_network (a wfdb-x addition) returns 1 if network access is + allowed, 0 otherwise. */ +FINT wfdbx_get_network(void) +{ + if (network < 0) { + const char *p = getenv("WFDBX_NETWORK"); + + env_network = (p != NULL && strtol(p, NULL, 10) != 0); + network = env_network; + } + return (network); +} + +/* setwfdb_normal is setwfdb in normal mode: the path p, split into components + as by wfdb_parse_path, must name at most one input directory. */ +static const char *path_component(const char *q, const char **start, + int *type); + +static int setwfdb_normal(const char *p) +{ + const char *q, *start; + char *d = NULL; + int n = 0, type; + + for (q = p ? p : ""; *q; ) { + q = path_component(q, &start, &type); + if (q > start) { + if (n++ == 0) { + SALLOC(d, q - start + 1, 1); + memcpy(d, start, q - start); + } + } + if (*q) q++; + } + if (n > 1) { + wfdb_error("setwfdb: WFDB-x reads from a single input directory; use " + "wfdbx_set_input_dir (or legacy mode for the WFDB search " + "path)\n"); + SFREE(d); + return (-1); + } + put_input_dir(d); + SFREE(setwfdb_input_dir); + setwfdb_input_dir = d; + setwfdb_called = 1; + return (0); +} + +/* wfdb-x: expand_home returns (in allocated memory) the name s, with a + leading "~" or "~/" replaced by the home directory ($HOME, or the home + directory of the current user if HOME is not set; on MS-Windows, + %USERPROFILE%), and, on POSIX systems, "~user" by the home directory of + that user (if there is no such user, s is unchanged), as Python's + os.path.expanduser does. */ +static char *expand_home(const char *s) +{ + const char *home = NULL, *rest = s; + char *r = NULL; + size_t n; + + if (*s == '~') { + for (rest = s + 1; *rest && !is_dsep(*rest); rest++) + ; + if (rest == s + 1) { +#ifdef _WIN32 + home = getenv("USERPROFILE"); +#else + struct passwd *pw; + + if ((home = getenv("HOME")) == NULL && (pw = getpwuid(getuid()))) + home = pw->pw_dir; +#endif + } +#ifndef _WIN32 + else { + char *user = NULL; + struct passwd *pw; + + SALLOC(user, rest - s, 1); + memcpy(user, s + 1, rest - s - 1); + if ((pw = getpwnam(user)) != NULL) + home = pw->pw_dir; + SFREE(user); + } +#endif + } + if (home == NULL) { + SSTRCPY(r, s); + return (r); + } + for (n = strlen(home); n > 0 && is_dsep(home[n-1]); n--) + ; + wfdb_asprintf(&r, "%.*s%s", (int)n, home, rest); + if (r && *r == '\0') + SSTRCPY(r, "/"); + return (r); +} + /* wfdbfile returns the pathname or URL of a WFDB file. */ FSTRING wfdbfile(const char *s, char *record) @@ -494,10 +810,58 @@ OS- and compiler-dependent definitions: /* wfdb_parse_path constructs a linked list of path components by splitting its string input (usually the value of WFDB). */ +/* wfdb-x: path_component finds the next component of the path string q (from + q, after any whitespace): it sets *start to its beginning and *type to its + type (WFDB_LOCAL or WFDB_NET), and returns a pointer to its end (the + character that follows it). (Formerly in the body of wfdb_parse_path; + also used by setwfdb in normal mode.) */ +static const char *path_component(const char *q, const char **start, + int *type) +{ + int slashes, found_end; + + /* Find the beginning of the next component (skip whitespace). */ + while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') + q++; + *start = q--; + *type = WFDB_LOCAL; + /* Find the end of the current component. */ + found_end = 0; + slashes = 0; + do { + switch (*++q) { + case ':': /* might be a component delimiter, part of '://', + a drive suffix (MS-DOS), or a directory separator + (Mac) */ + if (*(q+1) == '/' && *(q+2) == '/') *type = WFDB_NET; +#if PSEP == ':' + /* Allow colons within the authority portion of the URL. + For example, "http://[::1]:8080/database:/usr/database" + is a database path with two components. */ + else if (*type != WFDB_NET || slashes > 2) + found_end = 1; +#endif + break; + case ';': /* definitely a component delimiter */ + case ' ': + case '\t': + case '\n': + case '\r': + case '\0': + found_end = 1; + break; + case '/': + slashes++; + break; + } + } while (!found_end); + return (q); +} + int wfdb_parse_path(const char *p) { const char *q; - int current_type, slashes, found_end; + int current_type; struct wfdb_path_component *c0 = NULL, *c1 = wfdb_path_list; static first_call = 1; @@ -516,41 +880,7 @@ int wfdb_parse_path(const char *p) /* Now construct the wfdb_path_list from the contents of p. */ while (*q) { - /* Find the beginning of the next component (skip whitespace). */ - while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') - q++; - p = q--; - current_type = WFDB_LOCAL; - /* Find the end of the current component. */ - found_end = 0; - slashes = 0; - do { - switch (*++q) { - case ':': /* might be a component delimiter, part of '://', - a drive suffix (MS-DOS), or a directory separator - (Mac) */ - if (*(q+1) == '/' && *(q+2) == '/') current_type = WFDB_NET; -#if PSEP == ':' - /* Allow colons within the authority portion of the URL. - For example, "http://[::1]:8080/database:/usr/database" - is a database path with two components. */ - else if (current_type != WFDB_NET || slashes > 2) - found_end = 1; -#endif - break; - case ';': /* definitely a component delimiter */ - case ' ': - case '\t': - case '\n': - case '\r': - case '\0': - found_end = 1; - break; - case '/': - slashes++; - break; - } - } while (!found_end); + q = path_component(q, &p, ¤t_type); /* current component begins at p, ends at q-1 */ SUALLOC(c1, 1, sizeof(struct wfdb_path_component)); @@ -946,8 +1276,96 @@ int wfdb_fprintf(WFDB_FILE *wp, const char *format, ...) wfdb_asprintf(S, "%s.%.3s", RECORD, TYPE)) #endif +/* wfdb-x: join returns (in allocated memory) dir + sep + name, with exactly + one separator between them ('/' in a URL, otherwise sep). */ +static char *join(const char *dir, const char *name, int sep) +{ + char *r = NULL; + size_t n = strlen(dir); + + if (is_url(dir)) sep = '/'; + if (n == 0 || dir[n-1] == sep || is_dsep(dir[n-1])) + wfdb_asprintf(&r, "%s%s", dir, name); + else + wfdb_asprintf(&r, "%s%c%s", dir, sep, name); + return (r); +} + +/* wfdb-x: resolve returns (in allocated memory) the location of the file + named name in normal mode: an absolute name or a URL as given, a ~ name + expanded, and a relative name under the input directory (for reading) or + the output directory (for writing), if one is set. */ +static char *resolve(const char *name, int output) +{ + char *dir, *d, *r = NULL; + + if (*name == '~') + return (expand_home(name)); + if (is_absolute_path(name) || is_url(name)) { + SSTRCPY(r, name); + return (r); + } + if (output) { + init_output_dir(); + dir = output_dir; + } + else { + init_input_dir(); + dir = input_dir; + } + if (dir == NULL) { + SSTRCPY(r, name); + return (r); + } + d = expand_home(dir); + r = join(d, name, output ? DSEP : '/'); + SFREE(d); + return (r); +} + +/* wfdb-x: wfdbx_in_header_dir returns 1 if a file name found in a header + names a file in the header's directory or below it: a relative name that + does not begin with '~' and contains no ".." component (on MS-Windows, no + drive letter either). */ +int wfdbx_in_header_dir(const char *name) +{ + const char *p, *q; + + if (is_absolute_path(name) || is_url(name) || *name == '~') + return (0); +#ifdef _WIN32 + if (*name && name[1] == ':') + return (0); +#endif + for (p = name; ; p = q + 1) { + for (q = p; *q && !is_dsep(*q); q++) + ; + if (q - p == 2 && p[0] == '.' && p[1] == '.') + return (0); + if (*q == '\0') + return (1); + } +} + +/* wfdb-x: wfdbx_dirname returns (in allocated memory) the directory part of + the file name or URL s, including its final separator ("" if s has + none). */ +char *wfdbx_dirname(const char *s) +{ + const char *p; + char *r = NULL; + + for (p = s + strlen(s); p > s && !is_dsep(p[-1]); p--) + ; + SALLOC(r, p - s + 1, 1); + if (r) memcpy(r, s, p - s); + return (r); +} + static char irec[WFDB_MAXRNL+1]; /* current record name, set by wfdb_setirec */ +static WFDB_FILE *open_at(const char *p, const char *s); + /* wfdb_open is used by other WFDB library functions to open a database file for reading or writing. wfdb_open accepts two string arguments and an integer argument. The first string specifies the file type ("hea", "atr", etc.), @@ -1004,6 +1422,55 @@ Pre-10.0.1 versions of this library that were compiled for environments other than MS-DOS used file names in the format TYPE.RECORD. This file name format is no longer supported. */ +/* wfdb-x: expand_record returns (in allocated memory) the record name as + wfdb_open uses it: if the name ends with '/', another copy of its final + element is added (e.g., 'abc/123/' becomes 'abc/123/123'). */ +static char *expand_record(const char *record) +{ + char *p, *q, *r; + int rlen; + + rlen = strlen(record); + p = (char *)(record + rlen - 1); + if (rlen > 1 && *p == '/') { + for (q = p-1; q > record; q--) + if (*q == '/') { q++; break; } + if (q < p) { + SUALLOC(r, rlen + p-q + 1, 1); /* p-q is length of final element */ + strcpy(r, record); + strncpy(r + rlen, q, p-q); + } + else { + SUALLOC(r, rlen + 1, 1); + strcpy(r, record); + } + } + else { + SUALLOC(r, rlen + 1, 1); + strcpy(r, record); + } + return (r); +} + +/* wfdb-x: wfdbx_output_file returns (in allocated memory) the name of the + local file that wfdb_open(name, NULL, WFDB_WRITE) creates in normal mode, + or NULL if it creates none (the standard output, or a URL). */ +char *wfdbx_output_file(const char *name) +{ + char *r, *p; + + if (name == NULL || *name == '\0' || strcmp(name, "-") == 0) + return (NULL); + r = expand_record(name); + if (is_url(r)) { + SFREE(r); + return (NULL); + } + p = resolve(r, 1); + SFREE(r); + return (p); +} + WFDB_FILE *wfdb_open(const char *s, const char *record, int mode) { char *wfdb, *p, *q, *r, *buf = NULL; @@ -1037,43 +1504,45 @@ WFDB_FILE *wfdb_open(const char *s, const char *record, int mode) wfdb_stdout.type = WFDB_LOCAL; wfdb_stdout.fp = stdout; + wfdb_stdout.out = 1; return (&wfdb_stdout); } /* If the record name ends with '/', expand it by adding another copy of the final element (e.g., 'abc/123/' becomes 'abc/123/123'). */ - rlen = strlen(record); - p = (char *)(record + rlen - 1); - if (rlen > 1 && *p == '/') { - for (q = p-1; q > record; q--) - if (*q == '/') { q++; break; } - if (q < p) { - SUALLOC(r, rlen + p-q + 1, 1); /* p-q is length of final element */ - strcpy(r, record); - strncpy(r + rlen, q, p-q); - } - else { - SUALLOC(r, rlen + 1, 1); - strcpy(r, record); - } - } - else { - SUALLOC(r, rlen + 1, 1); - strcpy(r, record); - } + r = expand_record(record); /* wfdb-x: moved to expand_record */ /* If the file is to be opened for output, use the current directory. An output file can be opened in another directory if the path to that directory is the first part of 'record'. */ - if (mode == WFDB_WRITE) { + if (mode == WFDB_WRITE || mode == WFDB_APPEND) { spr1(&wfdb_filename, r, s); SFREE(r); - return (wfdb_fopen(wfdb_filename, WB)); + init_output_dir(); + if (!wfdbx_get_legacy()) { + /* wfdb-x: a relative name in the output directory, if one is set; + a URL cannot be written */ + if (is_url(wfdb_filename)) + return (NULL); + r = resolve(wfdb_filename, 1); + SFREE(wfdb_filename); + wfdb_filename = r; + } + /* wfdb-x: legacy mode: any name in the output directory, if set */ + else if (output_dir) + wfdb_asprintf(&wfdb_filename, "%s%c%s", output_dir, DSEP, + wfdb_filename); + return (wfdb_fopen(wfdb_filename, mode == WFDB_WRITE ? WB : AB)); } - else if (mode == WFDB_APPEND) { - spr1(&wfdb_filename, r, s); + + /* wfdb-x: in normal mode, the file is opened at the one location that its + name designates (see resolve); nothing is searched. */ + if (!wfdbx_get_legacy()) { + p = resolve(r, 0); SFREE(r); - return (wfdb_fopen(wfdb_filename, AB)); + ifile = open_at(p, s); + SFREE(p); + return (ifile); } /* Parse the WFDB path if not done previously. */ @@ -1190,12 +1659,67 @@ WFDB_FILE *wfdb_open(const char *s, const char *record, int mode) return (NULL); } +/* wfdb-x: open_at opens the file at location p (as resolved by resolve, or + in the directory of a header) for reading: it tries the two forms of the + name (see spr1 and spr2 above) there. */ +static WFDB_FILE *open_at(const char *p, const char *s) +{ + char *long_filename = NULL; + WFDB_FILE *ifile; + + if (s == NULL) s = ""; + spr1(&wfdb_filename, p, s); + if ((ifile = wfdb_fopen(wfdb_filename, RB)) != NULL) + return (ifile); + SSTRCPY(long_filename, wfdb_filename); + spr2(&wfdb_filename, p, s); + if (strcmp(wfdb_filename, long_filename)) + ifile = wfdb_fopen(wfdb_filename, RB); + SFREE(long_filename); + return (ifile); +} + +/* wfdb-x: wfdbx_open_in opens, for reading, a file named in the header of + record RECORD (a signal file, or a segment of a multi-segment record, of + type S): NAME, in the directory DIR of that header (as returned by + wfdbx_dirname; NULL if unknown, for a header read from the standard input: + then NAME is resolved as any relative name). NAME must be in that + directory or below it; the signal file name "-" (the standard input) is + also accepted. */ +WFDB_FILE *wfdbx_open_in(const char *dir, const char *s, const char *name, + const char *record) +{ + char *p = NULL; + WFDB_FILE *ifile; + + if (strcmp(name, "-") == 0) + return (wfdb_open(s, name, WFDB_READ)); + if (!wfdbx_in_header_dir(name)) { + wfdb_error("init: file %s named in the header of record %s is not " + "in the header's directory\n", name, record); + return (NULL); + } + if (dir == NULL) + return (wfdb_open(s, name, WFDB_READ)); + wfdb_asprintf(&p, "%s%s", dir, name); + ifile = open_at(p, s); + SFREE(p); + return (ifile); +} + /* wfdb_checkname checks record and annotator names -- they must not be empty, and they must contain only letters, digits, hyphens, tildes, underscores, and directory separators. */ int wfdb_checkname(const char *p, const char *s) { + /* wfdb-x: a record name may be a path; outside legacy mode only its last + component (the record's own name) is checked, so that directories + with other characters (such as "my.data/rec") can be written. */ + if (!wfdbx_get_legacy() && strcmp(s, "record") == 0) { + const char *q = strrchr(p, DSEP); + if (q) p = q + 1; + } do { if (('0' <= *p && *p <= '9') || *p == '_' || *p == '~' || *p== '-' || *p == DSEP || @@ -2163,29 +2687,306 @@ int wfdb_putc(int c, WFDB_FILE *wp) return (putc(c, wp->fp)); } +static int nwrite_errors; /* wfdb-x: see wfdbx_write_errors */ +static int stdout_closed; /* wfdb-x: stdout closed by wfdb_fclose */ + int wfdb_fclose(WFDB_FILE *wp) { - int status; + int status, err = 0; + /* wfdb-x: an output file that could not be written, flushed or closed + counts as a write error (see wfdbx_write_errors). */ + if (wp->out && wp->type != WFDB_NET && ferror(wp->fp)) + err = 1; + if (wp->fp == stdout) + stdout_closed = 1; #if WFDB_NETFILES status = (wp->type == WFDB_NET) ? nf_fclose(wp->netfp) : fclose(wp->fp); #else status = fclose(wp->fp); #endif + if (wp->out && (err || status)) + nwrite_errors++; if (wp->fp != stdin) SFREE(wp); return (status); } +/* wfdb-x: write errors. wfdbx_write_errors returns the number of write + errors since the program started: the output files that the library could + not write, flush or close (found when they are closed), and the errors that + applications report with wfdbx_note_write_error (for the files that they + write themselves). Decided by Reza Sameni, Oct 2026: a program must not + report success when its output was not written. */ +FINT wfdbx_write_errors(void) +{ + return (nwrite_errors); +} + +FVOID wfdbx_note_write_error(void) +{ + nwrite_errors++; +} + +/* wfdb-x: wfdbx_fclose_output closes fp (a FILE *), a file named name that an + application has written itself, and returns the result of fclose. In + normal mode, if the file could not be written or closed, it prints + "PNAME: write error on NAME" and notes a write error. */ +FINT wfdbx_fclose_output(void *fp, const char *pname, const char *name) +{ + int err = ferror((FILE *)fp), status = fclose((FILE *)fp); + + if ((err || status) && !wfdbx_get_legacy()) { + (void)fprintf(stderr, "%s: write error on %s\n", pname, name); + nwrite_errors++; + } + return (status); +} + +/* wfdb-x: the exit status of the applications. In normal mode, at exit, the + standard output is flushed; if it could not be written, the message + "PROGRAM: write error on standard output" is printed. Then, if the exit + status is 0 and the standard output or another output file could not be + written (see wfdbx_write_errors), the exit status is 1, as for the standard + Unix tools. Legacy mode (--legacy) keeps the original exit status. The + applications enable this through wfdbx_legacy_args. */ +static char *exit_pname; + +static void exit_check(int status, void *arg) +{ + int failed = 0; + + (void)arg; + if (wfdbx_get_legacy()) + return; + if (!stdout_closed && (fflush(stdout) == EOF || ferror(stdout))) { + (void)fprintf(stderr, "%s: write error on standard output\n", + exit_pname); + failed = 1; + } + if (status == 0 && (failed || nwrite_errors > 0)) { + (void)fflush(NULL); + _exit(1); + } +} + +void wfdbx_exit_check_init(const char *argv0) +{ + const char *p; + + if (exit_pname) + return; + p = strrchr(argv0, '/'); + SSTRCPY(exit_pname, p ? p + 1 : argv0); + on_exit(exit_check, NULL); +} + +/* wfdb-x: remote files. In normal mode, a URL (http:// or https://) is opened + only if network access is allowed (see wfdbx_set_network). It is then read + in full (no range requests) by the system program curl, run without a + shell as + curl --silent --fail --location --connect-timeout 30 + --speed-limit 1 --speed-time 30 URL + (following redirections, failing on an HTTP status of 400 or more, and + giving up if connecting takes 30 seconds or no data arrives for 30 + seconds; there is no limit on the total time, since record files can be + large), + reading its standard output. The result (the contents, or a failure) is + kept for the rest of the process, so that each URL is requested at most + once. The file is read from a temporary copy. */ + +struct fetched { + char *url; + char *data; + size_t len; + int ok; + struct fetched *next; +}; +static struct fetched *fetched_list; + +#include +#ifndef _WIN32 +#include +#include +#include +extern char **environ; +#else +#include +#include +#include +#endif + +/* run_curl reads the URL into *data (allocated; *len bytes). It returns 1 + on success, 0 on failure, or -1 if curl could not be started. */ +static int run_curl(const char *url, char **data, size_t *len) +{ + char *argv[12]; + size_t size = 0; + int status; + + argv[0] = "curl"; + argv[1] = "--silent"; + argv[2] = "--fail"; + argv[3] = "--location"; + argv[4] = "--connect-timeout"; + argv[5] = "30"; + argv[6] = "--speed-limit"; + argv[7] = "1"; + argv[8] = "--speed-time"; + argv[9] = "30"; + argv[10] = (char *)url; + argv[11] = NULL; + *data = NULL; + *len = 0; +#ifndef _WIN32 + { + int fd[2], err; + ssize_t n; + pid_t pid; + posix_spawn_file_actions_t fa; + + if (pipe(fd) != 0) + return (0); + posix_spawn_file_actions_init(&fa); + posix_spawn_file_actions_addopen(&fa, 0, "/dev/null", O_RDONLY, 0); + posix_spawn_file_actions_adddup2(&fa, fd[1], 1); + posix_spawn_file_actions_addopen(&fa, 2, "/dev/null", O_WRONLY, 0); + posix_spawn_file_actions_addclose(&fa, fd[0]); + posix_spawn_file_actions_addclose(&fa, fd[1]); + err = posix_spawnp(&pid, "curl", &fa, NULL, argv, environ); + posix_spawn_file_actions_destroy(&fa); + close(fd[1]); + if (err != 0) { + close(fd[0]); + return (-1); + } + for (;;) { + if (*len == size) { + size = size ? 2 * size : 65536; + SREALLOC(*data, size, 1); + } + if ((n = read(fd[0], *data + *len, size - *len)) > 0) + *len += n; + else if (n < 0 && errno == EINTR) + continue; + else + break; + } + close(fd[0]); + while (waitpid(pid, &status, 0) < 0) + if (errno != EINTR) + return (0); + if (!WIFEXITED(status)) + return (0); + status = WEXITSTATUS(status); + } +#else + { + /* The standard output of curl goes to a temporary file, and its + standard error to NUL (both redirected while it runs). */ + FILE *tf = tmpfile(); + int so, se, nul, c; + + if (tf == NULL) + return (0); + fflush(stdout); + fflush(stderr); + so = _dup(1); + se = _dup(2); + nul = _open("NUL", _O_WRONLY); + _dup2(_fileno(tf), 1); + if (nul >= 0) _dup2(nul, 2); + status = (int)_spawnvp(_P_WAIT, "curl", (const char *const *)argv); + _dup2(so, 1); + _dup2(se, 2); + _close(so); + _close(se); + if (nul >= 0) _close(nul); + if (status == -1) { + fclose(tf); + return (-1); + } + rewind(tf); + while ((c = getc(tf)) != EOF) { + if (*len == size) { + size = size ? 2 * size : 65536; + SREALLOC(*data, size, 1); + } + (*data)[(*len)++] = c; + } + fclose(tf); + } +#endif + if (status == 127) /* the shell's code for "command not found" */ + return (-1); + return (status == 0); +} + +/* fetch_open opens a URL for reading. */ +static WFDB_FILE *fetch_open(const char *url) +{ + static int network_warned, curl_warned; + struct fetched *f; + WFDB_FILE *wp; + FILE *fp; + + if (!wfdbx_get_network()) { + if (!network_warned) { + network_warned = 1; + wfdb_error("WFDB-x: network access is disabled; set " + "WFDBX_NETWORK=1 or call wfdbx_set_network(1) to open " + "%s\n", url); + } + return (NULL); + } + for (f = fetched_list; f; f = f->next) + if (strcmp(f->url, url) == 0) + break; + if (f == NULL) { + int stat; + + SUALLOC(f, 1, sizeof(struct fetched)); + SSTRCPY(f->url, url); + stat = run_curl(url, &f->data, &f->len); + if (stat < 0 && !curl_warned) { + curl_warned = 1; + wfdb_error("WFDB-x: network access needs the curl program, which " + "was not found\n"); + } + if ((f->ok = (stat > 0)) == 0) { + SFREE(f->data); + f->len = 0; + } + f->next = fetched_list; + fetched_list = f; + } + if (!f->ok || (fp = tmpfile()) == NULL) + return (NULL); + if (f->len > 0 && fwrite(f->data, 1, f->len, fp) != f->len) { + fclose(fp); + return (NULL); + } + rewind(fp); + SUALLOC(wp, 1, sizeof(WFDB_FILE)); + wp->fp = fp; + wp->type = WFDB_FETCHED; + return (wp); +} + WFDB_FILE *wfdb_fopen(char *fname, const char *mode) { char *p = fname; WFDB_FILE *wp; - if (p == NULL || strstr(p, "..")) + /* wfdb-x: in normal mode, names containing ".." are accepted, and an + http:// or https:// URL is fetched (see fetch_open); other names are + local files. */ + if (p == NULL || (wfdbx_get_legacy() && strstr(p, ".."))) return (NULL); + if (!wfdbx_get_legacy() && is_url(p)) + return (*mode == 'r' ? fetch_open(p) : NULL); SUALLOC(wp, 1, sizeof(WFDB_FILE)); - if (strstr(p, "://")) { + if (wfdbx_get_legacy() && strstr(p, "://")) { #if WFDB_NETFILES if (wp->netfp = nf_fopen(fname, mode)) { wp->type = WFDB_NET; @@ -2197,6 +2998,7 @@ WFDB_FILE *wfdb_fopen(char *fname, const char *mode) } if (wp->fp = fopen(fname, mode)) { wp->type = WFDB_LOCAL; + wp->out = (*mode != 'r'); return (wp); } if (strcmp(mode, WB) == 0 || strcmp(mode, AB) == 0) { diff --git a/lib/wfdblib.h0 b/lib/wfdblib.h0 index 49479a6..6b49c87 100644 --- a/lib/wfdblib.h0 +++ b/lib/wfdblib.h0 @@ -204,11 +204,14 @@ struct WFDB_FILE { FILE *fp; struct netfile *netfp; int type; + int out; /* wfdb-x: nonzero for an output file (see wfdb_fclose) */ }; /* Values for WFDB_FILE 'type' field */ #define WFDB_LOCAL 0 /* a local file, read via C standard I/O */ #define WFDB_NET 1 /* a remote file, read via libwww */ +#define WFDB_FETCHED 2 /* wfdb-x: a remote file, fetched in full and + read via C standard I/O (see wfdbio.c) */ /* Composite data types */ typedef struct netfile netfile; @@ -286,6 +289,7 @@ extern "C" { /* These functions are defined in wfdbio.c */ extern int wfdb_fclose(WFDB_FILE *fp); +extern void wfdbx_exit_check_init(const char *argv0); /* wfdb-x */ extern WFDB_FILE *wfdb_open(const char *file_type, const char *record, int mode); extern int wfdb_checkname(const char *name, const char *description); extern void wfdb_striphea(char *record); @@ -296,6 +300,11 @@ extern void wfdb_p32(long x, WFDB_FILE *fp); extern size_t wfdb_getline(char **buffer, size_t *buffer_size, WFDB_FILE *fp); extern int wfdb_parse_path(const char *wfdb_path); extern void wfdb_addtopath(const char *pathname); +extern WFDB_FILE *wfdbx_open_in(const char *dir, const char *type, + const char *name, const char *record); +extern char *wfdbx_dirname(const char *pathname); /* wfdb-x */ +extern char *wfdbx_output_file(const char *name); /* wfdb-x */ +extern int wfdbx_in_header_dir(const char *name); /* wfdb-x */ #if __GNUC__ >= 3 __attribute__((__format__(__printf__, 2, 3))) #endif @@ -338,15 +347,15 @@ extern void wfdb_oaflush(void); #else /* declare only function return types for non-ANSI C compilers */ -extern char *wfdb_getirec(); +extern char *wfdb_getirec(), *wfdbx_dirname(), *wfdbx_output_file(); extern int wfdb_fclose(), wfdb_checkname(), wfdb_g16(), wfdb_parse_path(), - wfdb_fprintf(); + wfdb_fprintf(), wfdbx_in_header_dir(); extern long wfdb_g32(); extern void wfdb_striphea(), wfdb_p16(), wfdb_p32(), wfdb_addtopath(), wfdb_setirec(), wfdb_sampquit(), wfdb_sigclose(), wfdb_osflush(), wfdb_freeinfo(), wfdb_oinfoclose(), wfdb_anclose(), wfdb_oaflush(); -extern WFDB_FILE *wfdb_open(), *wfdb_fopen(); +extern WFDB_FILE *wfdb_open(), *wfdb_fopen(), *wfdbx_open_in(); extern char *wfdb_fgets(); extern int wfdb_feof(), wfdb_ferror(), wfdb_fflush(), wfdb_fseek(),