e08ae97dc288749c2454fd98a10dc79f06e27cb5
max
  Tue Aug 11 06:16:20 2026 -0700
hgBlat/hgc: fixes from the v502 code review of the BLAT results pages, refs #37893

#4 blatOldTracks now falls back to "keep" (with a warning) on any value other
than keep/hide/delete, instead of dropping into the destructive delete branch,
so a typo in hg.conf can never silently discard a user's earlier BLAT tracks.

#5 htmlEncode() (js/utils.js) now also escapes " and ', which the browser's
text->markup conversion leaves alone. Every hgBlat caller puts the result in a
double-quoted attribute, so an unescaped quote in the cart position string could
break out of the attribute; the shared helper now honors its documented contract.

#6 The non-BLAT alignment title no longer starts with a stray space.

#7 open_memstream() is checked for NULL: on that failure the alignment renders
straight to stdout instead of writing to a NULL FILE and calling fclose(NULL).
The section reorder keys on the literal "<H4><A NAME=genomic>"/"<H4><A NAME=ali>"
strings, so a note was added in fuzzyShow.c and pslShow.c to keep them intact.

Found in the v502 final-build code review, refs #38069.

diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c
index fef34392649..98409105b14 100644
--- src/hg/hgc/hgc.c
+++ src/hg/hgc/hgc.c
@@ -9193,46 +9193,58 @@
        qName, chrom, tStartC, tEndC, idColor, ident, matchC, qSizeC, psl->strand);
 if (isNotEmpty(aliasStr))
     printf("<p>Genome sequence %s is also known as: %s.</p>\n", chrom, aliasStr);
 /* The shared library returns the number of alignment blocks it actually shows.  The DNA path merges
  * blocks separated by gaps <= 8 bases, so this can be fewer than psl->blockCount; use it (not
  * psl->blockCount) so the sidebar's "Block N" links match the #1..#N anchors that were emitted. */
 int blockCount;
 /* Capture the shared library's alignment HTML so we can reorder its sections for this page.  The
  * library emits them as Query (#cDNA), Genome (#genomic), then Side-by-side (#ali), with the
  * per-block anchors living inside the Genome section.  We want Query, Side-by-side, Genome so the
  * long per-block list sits at the bottom of both the page and the sidebar.  Reorder here, on the
  * server, rather than in JS, so the page does not reflow after it loads. */
 char *alnHtml = NULL;
 size_t alnLen = 0;
 FILE *alnF = open_memstream(&alnHtml, &alnLen);
+if (alnF == NULL)
+    {
+    /* open_memstream failed (out of memory): render straight to stdout, skipping the section
+     * reorder, rather than passing a NULL FILE to the renderer and then calling fclose(NULL). */
+    if (qType == gftRna || qType == gftDna)
+        blockCount = showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE);
+    else
+        blockCount = showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName);
+    }
+else
+    {
     if (qType == gftRna || qType == gftDna)
         blockCount = showPartialDnaAlignment(psl, oSeq, alnF, cdsS, cdsE, FALSE);
     else
         blockCount = showGfAlignment(psl, oSeq, alnF, qType, qStart, qEnd, qName);
     fclose(alnF);
     char *pGenome = (alnHtml != NULL) ? stringIn("<H4><A NAME=genomic>", alnHtml) : NULL;
     char *pAli    = (alnHtml != NULL) ? stringIn("<H4><A NAME=ali>", alnHtml) : NULL;
     if (pGenome != NULL && pAli != NULL && pGenome < pAli)
         {                                                /* Query, then Side-by-side, then Genome */
         fwrite(alnHtml, 1, pGenome - alnHtml, stdout);   /* legend + Query (#cDNA) section */
         fputs(pAli, stdout);                             /* Side-by-side (#ali) section, through footnote */
         fwrite(pGenome, 1, pAli - pGenome, stdout);      /* Genome (#genomic) section, with block anchors */
         }
     else
         fputs((alnHtml != NULL) ? alnHtml : "", stdout);
     free(alnHtml);   /* libc free: open_memstream's buffer is malloc'd, not a kent needMem block */
+    }
 printf("</div>\n");   /* #blatAlnContent */
 
 /* Sidebar, emitted after the alignment so blockCount is known; CSS grid puts it back in column 1.
  * The inner div is position:sticky so the links stay in view as the long alignment scrolls. */
 printf("<div id='blatAlnNav'><div id='blatAlnNavInner'>\n");
 printf("<a href='#cDNA'>Only query sequence</a>\n"
        "<a href='#ali'>Side by side alignment</a>\n"
        "<a href='#genomic'>Only genome sequence</a>\n");
 if (blockCount > 1)   /* per-block jump links, indented under the genome-sequence item where their anchors live */
     {
     int bi;
     printf("<div id='blatAlnBlocks'>\n");
     for (bi = 1;  bi <= blockCount;  ++bi)
         printf("<a href='#%d'>Block %d</a>\n", bi, bi);
     printf("</div>\n");
@@ -27494,30 +27506,40 @@
 if (isProt)
     extraForMismatch = "";
 char buffer[4096];
 safef(buffer, sizeof buffer, customTextTemplate, bigBedTn.forCgi, host, extraForMismatch, bigBedTn.forCgi, trackName, trackDescription);
 
 struct customTrack *ctList = getCtList();
 struct customTrack *newCts = customFactoryParse(database, buffer, FALSE, NULL, NULL);
 
 /* Optionally clear PREVIOUS BLAT result tracks (those tagged blatResult=on) so the user is not
  * confused about which results are current.  hg.conf "blatOldTracks":
  *   keep   (default) - do nothing, every search's track stays as-is
  *   hide             - leave earlier BLAT tracks in the session but set them to hide
  *   delete           - remove earlier BLAT tracks from the session (their trash files age out)
  * Only BLAT-tagged tracks are touched; the track just made is left alone. */
 char *oldTracks = cfgOptionDefault("blatOldTracks", "keep");
+/* Fail safe on an unrecognized value (e.g. a typo in hg.conf): fall back to "keep" rather than to
+ * the destructive "delete" branch below, so a misconfiguration never silently discards a user's
+ * previous BLAT tracks. */
+if (differentString(oldTracks, "keep") && differentString(oldTracks, "hide")
+    && differentString(oldTracks, "delete"))
+    {
+    warn("hg.conf blatOldTracks has unrecognized value '%s'; expected keep|hide|delete. "
+         "Treating as 'keep'.", oldTracks);
+    oldTracks = "keep";
+    }
 if (differentString(oldTracks, "keep"))
     {
     struct customTrack *ct, *next, *keptList = NULL;
     for (ct = ctList; ct != NULL; ct = next)
         {
         next = ct->next;
         if (ct->tdb != NULL && sameOk(trackDbSetting(ct->tdb, "blatResult"), "on"))
             {
             if (sameString(oldTracks, "hide"))
                 {
                 cartSetString(cart, ct->tdb->track, "hide");
                 slAddHead(&keptList, ct);   /* keep it in the session, just hidden */
                 }
             /* "delete": drop it from the list so customTracksSaveCart writes it out */
             }