c500eb92eb2a021af927f3c6f2ad00c7b29769a5 max Tue Sep 15 05:48:49 2026 -0700 snapshotSession: snapshotIsSnapshotName is file-local, refs #38353 Its only caller is the writer in the same file, checking its own argument, and its header comment warned everyone else off it. Making it static says that rather than asking. diff --git src/hg/lib/snapshotSession.c src/hg/lib/snapshotSession.c index c2a2aa728c8..17df5388123 100644 --- src/hg/lib/snapshotSession.c +++ src/hg/lib/snapshotSession.c @@ -1,358 +1,361 @@ /* snapshotSession - lightweight, shareable "view snapshot" sessions. See snapshotSession.h. */ /* Copyright (C) 2026 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "hash.h" #include "dystring.h" #include "cheapcgi.h" #include "jksql.h" #include "cart.h" #include "hgConfig.h" #include "portable.h" #include "md5.h" #include "htmshell.h" #include "sessionData.h" #include "trashDir.h" #include "snapshotSession.h" /* Bits of randomness in a server-generated snapshot token. 128 bits -> ~24 URL-safe chars, so * collisions are astronomically unlikely; snapshotNewName() also checks the DB and retries, so the * name is guaranteed unique regardless. */ #define snapshotTokenBits 128 /* ---- Registry of snapshot types ------------------------------------------------------------- */ /* BLAT: a single alignment (or the hgBlat results table) rebuilds from just the pinned bigPsl file; * the query sequence lives inside the bigPsl, so no .fa/.pslx is needed. "db" is added implicitly. */ static char *blatVars[] = { "blatLastBigBed", NULL }; static struct snapshotType snapshotTypes[] = { { "blat", blatVars, "blatLastBigBed" }, }; struct snapshotType *snapshotTypeFind(char *name) /* Return the registered snapshot type, or NULL if name is not a known type. */ { if (isEmpty(name)) return NULL; int i; for (i = 0; i < ArraySize(snapshotTypes); i++) if (sameString(name, snapshotTypes[i].name)) return &snapshotTypes[i]; return NULL; } boolean snapshotHasRequired(struct snapshotType *type, struct cart *cart) /* Return FALSE when type declares a requiredVar that is missing/empty in cart. */ { if (type == NULL || isEmpty(type->requiredVar)) return TRUE; return isNotEmpty(cartOptionalString(cart, type->requiredVar)); } -boolean snapshotIsSnapshotName(char *sessionName) -/* Return TRUE if sessionName is a snapshot name (starts with the "__" prefix). */ +static boolean snapshotIsSnapshotName(char *sessionName) +/* Return TRUE if sessionName is a snapshot name (starts with the "__" prefix). File-local: + * the only thing that may ask is the writer below, checking its own argument. A user's own + * session can carry the same prefix, so this must never be used to decide whether to show a + * row to its owner - snapshotIsSnapshotSettings() is the one for that. */ { return sessionName != NULL && startsWith(snapshotNamePrefix, sessionName); } char *snapshotTypeFromSettings(char *settings) /* See snapshotSession.h. Reads the one tag by walking the lines rather than calling * raFromString(): this runs once per row in the My Sessions listings, and building a hash there * costs ~600ns and three allocations per session that has any settings at all, against ~30ns and * none for the walk. Same line semantics as raFromString - leading blanks skipped, tag is the * first word, value is the rest of the line. */ { if (isEmpty(settings)) return NULL; int tagLen = strlen(snapshotTypeSetting); char *s = settings; while (s != NULL && s[0] != '\0') { s = skipLeadingSpaces(s); if (s == NULL || s[0] == '\0') break; if (startsWith(snapshotTypeSetting, s) && (s[tagLen] == ' ' || s[tagLen] == '\t')) { char *val = skipLeadingSpaces(s + tagLen); char *lineEnd = strchr(val, '\n'); char *type = (lineEnd != NULL) ? cloneStringZ(val, lineEnd - val) : cloneString(val); if (isEmpty(type)) freez(&type); return type; } s = strchr(s, '\n'); if (s != NULL) s++; } return NULL; } boolean snapshotIsSnapshotSettings(char *settings) /* See snapshotSession.h. */ { char *type = snapshotTypeFromSettings(settings); boolean isSnapshot = (type != NULL); freez(&type); return isSnapshot; } static char *snapshotSessionDirHashLen(char *sessionDataDir, char *encUserName, char *encSessionName, int hashLen) /* Alloc and return the durable data directory for one snapshot, or NULL if sessionDataDir is empty. * Like sessionData's sessionDirFromNames but with two extra hash levels drawn from the session name, * so a single high-volume user - the anonymous "l", which owns most snapshots - never accumulates * millions of entries in one directory. Layout: * sessionDataDir / <2hex md5(user)> / <encUser> / <2hex sess> / <2hex sess> / <hashLen hex sess> * The two fan-out levels are a prefix of the same hash, so they spread the entries out without * adding uniqueness: it is hashLen alone that decides how likely two snapshots are to land in the * same directory, which is why it is sessionDirHashLen and not 8. hashLen is passed in only so * that the cleaner can also name a directory written before it was widened. * Snapshots use their own layout (not sessionDirFromNames), so this never affects normal sessions. */ { if (isEmpty(sessionDataDir)) return NULL; if (sessionDataDir[0] != '/') errAbort("config setting sessionDataDir must be an absolute path (starting with '/')"); if (hashLen < 4 || hashLen > 32) errAbort("snapshotSessionDirHashLen: hashLen must be in [4,32], got %d", hashLen); char *userHash = md5HexForString(encUserName); char *sessHash = md5HexForString(encSessionName); char fan1[3], fan2[3]; safencpy(fan1, sizeof fan1, sessHash, 2); /* first 2 hex of the session hash */ safencpy(fan2, sizeof fan2, sessHash + 2, 2); /* next 2 hex -> 65536 buckets total */ userHash[2] = '\0'; sessHash[hashLen] = '\0'; struct dyString *dy = dyStringCreate("%s/%s/%s/%s/%s/%s", sessionDataDir, userHash, encUserName, fan1, fan2, sessHash); freeMem(userHash); freeMem(sessHash); return dyStringCannibalize(&dy); } static char *snapshotSessionDir(char *sessionDataDir, char *encUserName, char *encSessionName) /* Alloc and return the durable data directory for one snapshot, or NULL if sessionDataDir is * empty. */ { return snapshotSessionDirHashLen(sessionDataDir, encUserName, encSessionName, sessionDirHashLen); } char *snapshotNewName(struct sqlConnection *conn, char *encUserName) /* See snapshotSession.h. */ { char query[512]; int tries; for (tries = 0; tries < 100; tries++) { char *tok = makeRandomKey(snapshotTokenBits); char *name = catTwoStrings(snapshotNamePrefix, tok); freeMem(tok); sqlSafef(query, sizeof query, "select count(*) from %s where userName='%s' and sessionName='%s'", namedSessionTable, encUserName, name); if (sqlQuickNum(conn, query) == 0) return name; freez(&name); } errAbort("snapshotNewName: could not find an unused name for user '%s' after %d tries", encUserName, tries); return NULL; } /* ---- Saving --------------------------------------------------------------------------------- */ static void appendVar(struct dyString *dy, char *var, char *val) /* Append "var=cgiEncode(val)" to dy, with a leading '&' if dy is non-empty. */ { if (dy->stringSize > 0) dyStringAppendC(dy, '&'); dyStringAppend(dy, var); dyStringAppendC(dy, '='); char *e = cgiEncode(val); dyStringAppend(dy, e); freez(&e); } static char *snapshotMakeContents(struct snapshotType *type, char *encUserName, char *encSessionName, struct cart *cart) /* Build the minimal CGI-encoded contents for a snapshot: "db" plus the type's declared vars. For * each var present in the cart, move any backing trash file into durable sessionData storage (so the * snapshot outlives trash cleaning) and store the durable path. Returns a string to free. */ { char *sessionDataDir = cfgOption("sessionDataDir"); char *sessionDir = snapshotSessionDir(sessionDataDir, encUserName, encSessionName); struct dyString *dy = dyStringNew(512); char *db = cartOptionalString(cart, "db"); if (isNotEmpty(db)) appendVar(dy, "db", db); int i; for (i = 0; type->vars[i] != NULL; i++) { char *var = type->vars[i]; char *val = cartOptionalString(cart, var); if (isEmpty(val)) continue; /* Move the referenced trash file into durable storage when sessionData is configured, and store * the durable path. sessionDataSaveTrashFile returns NULL if the file is gone (expired) - in * that case keep the original value so the reconstruct path can report a clean "expired". * Only a trash path is ours to move, and the value came out of the cart, so check it the way * sessionData.c's own callers do. A value that is already a durable path (a snapshot being * re-shared) or anything else is stored as it stands. */ char *durable = NULL; if (isNotEmpty(sessionDir) && isTrashPath(val)) durable = sessionDataSaveTrashFile(val, sessionDir); appendVar(dy, var, isNotEmpty(durable) ? durable : val); freez(&durable); } freez(&sessionDir); return dyStringCannibalize(&dy); } int saveSnapshotSession(struct sqlConnection *conn, char *snapshotTypeName, char *encUserName, char *encSessionName, struct cart *cart) /* See snapshotSession.h. */ { struct snapshotType *type = snapshotTypeFind(snapshotTypeName); if (type == NULL) errAbort("saveSnapshotSession: unknown snapshot type '%s'", snapshotTypeName); if (!snapshotIsSnapshotName(encSessionName)) errAbort("saveSnapshotSession: name '%s' must start with the '%s' prefix", encSessionName, snapshotNamePrefix); char *contents = snapshotMakeContents(type, encUserName, encSessionName, cart); boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0); /* Preserve firstUse/useCount if we are overwriting an existing snapshot of this name. */ struct dyString *dy = dyStringNew(4096); char *firstUse = NULL; int useCount = 0; sqlDyStringPrintf(dy, "SELECT firstUse, useCount FROM %s WHERE userName='%s' AND sessionName='%s'", namedSessionTable, encUserName, encSessionName); struct sqlResult *sr = sqlGetResult(conn, dy->string); char **row; if ((row = sqlNextRow(sr)) != NULL) { firstUse = cloneString(row[0]); useCount = atoi(row[1]) + 1; } sqlFreeResult(&sr); dyStringClear(dy); sqlDyStringPrintf(dy, "DELETE FROM %s WHERE userName='%s' AND sessionName='%s'", namedSessionTable, encUserName, encSessionName); sqlUpdate(conn, dy->string); /* settings records the snapshot type, so the row is self-describing for reconstruction/debugging. * This is also what marks the row as a share token rather than a session the user saved: the * "__" name prefix cannot say that, because a user may have named a session of their own that way * (refs #38313). */ char settings[256]; safef(settings, sizeof settings, "%s %s\n", snapshotTypeSetting, type->name); dyStringClear(dy); sqlDyStringPrintf(dy, "INSERT INTO %s (userName, sessionName, contents, shared, firstUse, lastUse, useCount", namedSessionTable); if (gotSettings) sqlDyStringPrintf(dy, ", settings"); sqlDyStringPrintf(dy, ") VALUES ('%s', '%s', '", encUserName, encSessionName); sqlDyAppendEscaped(dy, contents); sqlDyStringPrintf(dy, "', 1, "); /* shared = 1 (shareable by link) */ if (firstUse) sqlDyStringPrintf(dy, "'%s', ", firstUse); else sqlDyStringPrintf(dy, "now(), "); sqlDyStringPrintf(dy, "now(), %d", useCount); if (gotSettings) { sqlDyStringPrintf(dy, ", '"); sqlDyAppendEscaped(dy, settings); sqlDyStringPrintf(dy, "'"); } sqlDyStringPrintf(dy, ")"); sqlUpdate(conn, dy->string); freez(&contents); freez(&firstUse); dyStringFree(&dy); return useCount; } /* ---- Cleaning up abandoned anonymous snapshots ---------------------------------------------- */ static void removeDirTree(char *dir) /* Best-effort recursive removal of a directory and its contents (files are hard-links into durable * storage; unlinking frees the space). Missing paths are ignored. */ { if (isEmpty(dir) || !fileExists(dir)) return; struct fileInfo *fiList = listDirX(dir, "*", TRUE), *fi; for (fi = fiList; fi != NULL; fi = fi->next) { if (fi->isDir) removeDirTree(fi->name); else remove(fi->name); } slFreeList(&fiList); rmdir(dir); } int snapshotCleanAnon(struct sqlConnection *conn, int ttlDays, boolean dryRun) /* See snapshotSession.h. */ { char *sessionDataDir = cfgOption("sessionDataDir"); char query[1024]; /* '\_\_%' : the two leading underscores are literal (escaped, since '_' is a LIKE wildcard), * followed by the '%' wildcard for the random token. */ sqlSafef(query, sizeof query, "SELECT sessionName FROM %s WHERE userName='%s' AND sessionName LIKE '\\_\\_%%' " "AND lastUse < DATE_SUB(now(), INTERVAL %d DAY)", namedSessionTable, snapshotAnonUser, ttlDays); struct slName *toClean = NULL; struct sqlResult *sr = sqlGetResult(conn, query); char **row; while ((row = sqlNextRow(sr)) != NULL) slNameAddHead(&toClean, row[0]); sqlFreeResult(&sr); int n = 0; struct slName *s; for (s = toClean; s != NULL; s = s->next) { if (!dryRun) { /* Remove the durable files first, then the row, so a crash never orphans the DB pointer. * Minimal snapshots live under the fanned-out snapshotSessionDir; a full anonymous share * (e.g. the top-right "Share a link" when logged out) uses sessionData's flat layout. Both * layouts changed name when sessionDirHashLen was widened from 8, so try the old spelling * too - otherwise a snapshot saved before the change would leave its files behind forever. * Remove whichever exist (removeDirTree ignores a missing path). */ char *dirs[4]; dirs[0] = snapshotSessionDir(sessionDataDir, snapshotAnonUser, s->name); dirs[1] = sessionDirFromNames(sessionDataDir, snapshotAnonUser, s->name); dirs[2] = snapshotSessionDirHashLen(sessionDataDir, snapshotAnonUser, s->name, sessionDirHashLenLegacy); dirs[3] = sessionDirFromNamesHashLen(sessionDataDir, snapshotAnonUser, s->name, sessionDirHashLenLegacy); int i; for (i = 0; i < ArraySize(dirs); i++) { removeDirTree(dirs[i]); freez(&dirs[i]); } sqlSafef(query, sizeof query, "DELETE FROM %s WHERE userName='%s' AND sessionName='%s'", namedSessionTable, snapshotAnonUser, s->name); sqlUpdate(conn, query); } n++; } slFreeList(&toClean); return n; }