554396e44745165ee4baf5214aa771f2b17b3be6
braney
  Mon Aug 17 16:04:26 2026 -0700
hgPcr, cart: screen the PCR result file names read back out of the cart, refs #37623

The hgPcrResult_<db> cart variable holds two file names and an optional target
name in one value.  The cart.c arrays compare a whole value against
isServerUserFilePath(), so none of them fit that shape.  Add a fourth array for
it and check the first two words.  hgPcrResult_targetStyle shares the prefix and
is a display setting, so it is excluded by name.

Check both names where they are used as well, in pcrResultParseCart() and in
hgPcr's append path, the way dupTrack.c already does.

Two other things in writePcrResultTrack().  pcrFiles[2] was read without ever
being set whenever the value held only two words, which is the usual case.  And
the saved-session test was a plain prefix compare that missed sessionDataDirOld;
it now asks whether the file is in the trash instead.

hg/utils/cartFileVarCatalog knows about the new array and has a row for
hgPcrResult_<db> saying why its scan cannot see this one.

diff --git src/hg/lib/cart.c src/hg/lib/cart.c
index c0c990defee..e22a6f491ca 100644
--- src/hg/lib/cart.c
+++ src/hg/lib/cart.c
@@ -25,30 +25,31 @@
 #include "wikiLink.h"
 #endif /* GBROWSE */
 #include "hgMaf.h"
 #include "hui.h"
 #include "geoMirror.h"
 #include "hubConnect.h"
 #include "trackHub.h"
 #include "cgiApoptosis.h"
 #include "customComposite.h"
 #include "regexHelper.h"
 #include "windowsToAscii.h"
 #include "jsonWrite.h"
 #include "verbose.h"
 #include "genark.h"
 #include "quickLift.h"
+#include "pcrResult.h"
 #include "botDelay.h"
 #include "curlWrap.h"
 #include "hubSpaceKeys.h"
 #include "myVariantsShare.h"
 #include "customTrack.h"
 #include "dupTrack.h"
 #include "myVariants.h"
 
 static char *sessionVar = "hgsid";	/* Name of cgi variable session is stored in. */
 static char *positionCgiName = "position";
 
 DbConnector cartDefaultConnector = hConnectCart;
 DbDisconnect cartDefaultDisconnector = hDisconnectCart;
 static boolean cartDidContentType = FALSE;
 
@@ -235,53 +236,92 @@
     MYVARIANTS_FILE_VAR_PREFIX,         // mvCtfile_<db>: myVariants custom track
     customCompositeCartName "-",        // customComposite-<db>: track collection hub
     quickLiftCartName "-",              // hubQuickLift-<db>: quickLift hub
 };
 
 /* These two hold either a remote URL or the name of a file the server made, and the code that
  * reads them tells the cases apart by looking for a protocol.  A value with no protocol falls
  * through to opening a local file, so it has to be one of ours; a real URL is fine. */
 
 static char *urlOrFileNameCartVars[] =
 {
     "multiRegionsBedUrl",       // hgTracks multi-region BED: a URL, or the trash file we wrote
     hgsLoadUrlName,             // hgSession load settings from URL
 };
 
+/* This one does not hold a single file name.  hgPcr writes two file names and an optional
+ * target name into it, separated by spaces: "<psl file> <primer file> [<targetDb name>]".
+ * Both file names are checked; the target name is not a path.  hgPcr appends to both files
+ * when the user asks to add to an existing result, so a retargeted value is a write as well
+ * as a read. */
+
+static char *fileNamePairCartVarPrefixes[] =
+{
+    PCR_RESULT_TRACK_NAME "_",  // hgPcrResult_<db>: in-silico PCR result files
+};
+
 static boolean cartVarHoldsFileName(char *var)
 /* Return TRUE if var is one of the cart variables listed above. */
 {
 int i;
 for (i = 0;  i < ArraySize(fileNameCartVars);  i++)
     if (sameString(var, fileNameCartVars[i]))
         return TRUE;
 for (i = 0;  i < ArraySize(fileNameCartVarPrefixes);  i++)
     if (startsWith(fileNameCartVarPrefixes[i], var))
         return TRUE;
 return FALSE;
 }
 
 static boolean cartVarHoldsUrlOrFileName(char *var)
 /* Return TRUE if var is one of the cart variables that may hold either. */
 {
 int i;
 for (i = 0;  i < ArraySize(urlOrFileNameCartVars);  i++)
     if (sameString(var, urlOrFileNameCartVars[i]))
         return TRUE;
 return FALSE;
 }
 
+static boolean cartVarHoldsFileNamePair(char *var)
+/* Return TRUE if var is one of the variables that hold a pair of file names.
+ * hgPcrResult_targetStyle is a display setting that shares the hgPcrResult_ prefix with the
+ * per-db result variables, so it is excluded by name. */
+{
+if (sameString(var, PCR_RESULT_TARGET_STYLE))
+    return FALSE;
+int i;
+for (i = 0;  i < ArraySize(fileNamePairCartVarPrefixes);  i++)
+    if (startsWith(fileNamePairCartVarPrefixes[i], var))
+        return TRUE;
+return FALSE;
+}
+
+static boolean fileNamePairIsAcceptable(char *val)
+/* Return TRUE if the first two whitespace-separated words of val both name a file the server
+ * made for this user.  Any other shape is refused: the code that reads this value errAborts
+ * on fewer than two words, and reads no more than three. */
+{
+char *dupe = cloneString(val);
+char *words[4];
+int wordCount = chopByWhite(dupe, words, ArraySize(words));
+boolean ok = (wordCount == 2 || wordCount == 3) &&
+             isServerUserFilePath(words[0]) && isServerUserFilePath(words[1]);
+freeMem(dupe);
+return ok;
+}
+
 static void logDroppedFileNameVar(char *var, char *why)
 /* Note the drop in the error log, so that a false positive can be spotted after release.
  * The variable name comes from the user, so copy out only characters that cannot forge a
  * log line of their own, and keep the copy short. */
 {
 char clean[129];
 int i;
 for (i = 0;  i < (int)sizeof(clean) - 1 && var[i] != 0;  i++)
     {
     unsigned char c = var[i];
     clean[i] = (isalnum(c) || c == '_' || c == '.' || c == '-') ? c : '?';
     }
 clean[i] = 0;
 fprintf(stderr, "cart: dropped %s, value is not %s\n", clean, why);
 }
@@ -294,30 +334,37 @@
     return TRUE;
 if (cartVarHoldsFileName(var))
     {
     if (isServerUserFilePath(val))
         return TRUE;
     logDroppedFileNameVar(var, "a trash or session-data file name");
     return FALSE;
     }
 if (cartVarHoldsUrlOrFileName(var))
     {
     if (isServerUserFileOrUrl(val))
         return TRUE;
     logDroppedFileNameVar(var, "a URL or a trash or session-data file name");
     return FALSE;
     }
+if (cartVarHoldsFileNamePair(var))
+    {
+    if (fileNamePairIsAcceptable(val))
+        return TRUE;
+    logDroppedFileNameVar(var, "a pair of trash or session-data file names");
+    return FALSE;
+    }
 return TRUE;
 }
 
 static void loadHash(struct hash *hash, char *contents)
 /* Load a hash from a cart-like string. */
 {
 char *namePt, *dataPt, *nextNamePt;
 namePt = contents;
 while (namePt != NULL && namePt[0] != 0)
     {
     dataPt = strchr(namePt, '=');
     if (dataPt == NULL)
 	errAbort("Mangled input string %s", namePt);
     *dataPt++ = 0;
     nextNamePt = strchr(dataPt, '&');