diff --git a/overlay/Scripts/Custom/Bridge/BridgeArt.cs b/overlay/Scripts/Custom/Bridge/BridgeArt.cs
index 3cdbfe1..c00ef8c 100644
--- a/overlay/Scripts/Custom/Bridge/BridgeArt.cs
+++ b/overlay/Scripts/Custom/Bridge/BridgeArt.cs
@@ -175,7 +175,7 @@ namespace Server.Custom.Bridge
string id = SourceId();
- if (expected != null && expected != id)
+ if (BridgeAssets.CatalogMismatch(expected, id))
{
BridgeAssets.Fail(reqId, "UNREADABLE",
"the shard's client files changed since that catalogue was read (catalog "
diff --git a/overlay/Scripts/Custom/Bridge/BridgeAssets.cs b/overlay/Scripts/Custom/Bridge/BridgeAssets.cs
index 4b68f68..1490de0 100644
--- a/overlay/Scripts/Custom/Bridge/BridgeAssets.cs
+++ b/overlay/Scripts/Custom/Bridge/BridgeAssets.cs
@@ -670,6 +670,30 @@ namespace Server.Custom.Bridge
}
}
+ ///
+ /// Does a caller's asserted catalog id disagree with what this shard holds?
+ ///
+ /// **An absent fingerprint and an empty one mean the same thing**, and that is the
+ /// whole reason this is a function rather than an inline `expected != null`. A caller
+ /// with nothing to assert sends the field absent or empty depending on how its own
+ /// client serialises a missing value, and treating `""` as a real id refuses **every**
+ /// fetch it makes — with a sentence naming no catalog at all ("catalog is now
+ /// 8159778b"), which reads as a shard fault rather than a caller one.
+ ///
+ /// Phase 7 found this on the tree family, where a probe passed an empty string by
+ /// accident. It was inline in three places by then; it is one function now, because
+ /// three copies of a comparison are three chances for the next family to get it wrong
+ /// in a way only a differently-written client would ever reveal.
+ ///
+ /// Note this is deliberately NOT the shape `BridgeLeases` uses for its own `expected`:
+ /// there the value is a world property, where an empty string is a legitimate thing to
+ /// expect and `!= null` is correct.
+ ///
+ internal static bool CatalogMismatch(string expected, string actual)
+ {
+ return !String.IsNullOrEmpty(expected) && !String.Equals(expected, actual, StringComparison.Ordinal);
+ }
+
///
/// SHA-256, lowercase hex. Shared because the hash in a manifest row, the hash in a
/// fetch row and the hash the website stores must be one function.
diff --git a/overlay/Scripts/Custom/Bridge/BridgeCatalog.cs b/overlay/Scripts/Custom/Bridge/BridgeCatalog.cs
index d24e868..859bc89 100644
--- a/overlay/Scripts/Custom/Bridge/BridgeCatalog.cs
+++ b/overlay/Scripts/Custom/Bridge/BridgeCatalog.cs
@@ -357,7 +357,7 @@ namespace Server.Custom.Bridge
string id = SourceId();
- if (expected != null && expected != id)
+ if (BridgeAssets.CatalogMismatch(expected, id))
{
// The client files moved between the manifest and this fetch. Refusing is the only
// honest answer: the keys were chosen against a catalogue that no longer describes
diff --git a/overlay/Scripts/Custom/Bridge/BridgeTree.cs b/overlay/Scripts/Custom/Bridge/BridgeTree.cs
index c6e27a7..0727cdd 100644
--- a/overlay/Scripts/Custom/Bridge/BridgeTree.cs
+++ b/overlay/Scripts/Custom/Bridge/BridgeTree.cs
@@ -421,12 +421,10 @@ namespace Server.Custom.Bridge
List files = Enumerate();
string fingerprint = FingerprintOf(files);
- // `IsNullOrEmpty`, not `!= null`. A caller that has no fingerprint to assert sends
- // the field absent OR empty depending on how its own client serialises a missing
- // value, and the two must mean the same thing — an empty string compared against a
- // real id refuses every fetch, with a sentence that names no catalog at all
- // ("catalog is now 8159778b"). Found by a probe that passed one.
- if (!String.IsNullOrEmpty(expected) && expected != fingerprint)
+ // Shared with every other family on this plane, because an absent fingerprint and an
+ // empty one have to mean the same thing here and there — see
+ // `BridgeAssets.CatalogMismatch` for what treating them differently costs.
+ if (BridgeAssets.CatalogMismatch(expected, fingerprint))
{
// The tree moved between the manifest and this fetch. The same refusal the
// catalogue makes for a patched client, and for the same reason: these keys were